Está en la página 1de 34

CUDA

Manuel Ujaldn Martnez


Departamento de Arquitectura de Computadores
Universidad de Mlaga
2
Agradecimientos
Al personal de Nvidia, por compartir conmigo ideas,
material, diagramas, presentaciones, ... Alfabticamente:
Bill Dally [2010-2011: Consumo energtico, Echelon y diseos futuros].
Simon Green [2007-2009: Los pilares de CUDA].
Sumit Gupta [2008-2009: El hardware de Tesla].
Mark Harris [2008, 2012: CUDA, OpenACC, Lenguajes de Programacin, Libreras].
Wen-Mei Hwu [2009: Programacin y trucos para mejorar el rendimiento].
Stephen Jones [2012: Kepler].
David B. Kirk [2008-2009: Hardware de Nvidia].
Timothy Lanfear [2012: Kepler y OpenACC].
David Luebke [2007-2008: Hardware de Nvidia].
Lars Nyland [2012: Kepler].
Edmondo Orlotti [2012: CUDA 5.0, OpenACC].
Cyril Zeller [2008-2009: Fundamentos de CUDA].
... por mencionar slo unos pocos del equipo que contriby a esta
presentacin.
Contenidos del tutorial
1. Introducin. [17 diapositivas]
2. Arquitectura. [19]
1. El modelo hardware de CUDA. [4]
2. La primera generacin: Tesla (2006-2009). [5]
3. La segunda generacin: Fermi (2010-2011). [5]
4. La tercera generacin: Kepler (2012-2014). [5]
3. Programacin. [15]
4. Sintaxis. [16]
1. Elementos bsicos. [10]
2. Un par de ejemplos preliminares. [6]
5. Compilacin. [13]
6. Una serie de ejemplos: VectorAdd, Stencil, ReverseArray,
MxM. [1+5+9+4+10]
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.
4
I. Introduccin
5
Bienvenido al mundo de las GPUs
6
En apenas 5 aos, la programacin CUDA ha
crecido a un ritmo vertiginoso. As, en 2013:
Se han publicado ms de 40.000 artculos cientficos.
CUDA se ensea en ms de 650 universidades.
Hay ms de 450 millones de GPUs programables con CUDA.
Ms de 150.000 desarrolladores/programadores en activo.
Ms de 1.700.000 descargas del compilador y SDK.
7
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.
8
9
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.
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 cientos de threads.
CUDA define:
Un modelo de arquitectura:
Con multitud de unidades de proceso (cores) y una sola unidad de control (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 de forma sencilla,
permitiendo declarar miles de hilos.
Permitir computacin heterognea en CPU y GPU. 10
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.
Computacin heterognea (1/4)
Host Device
11
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.
Computacin heterognea (2/4)
12
CPU (host)
GPU
(device)
Memoria principal
(DDR3)
Memoria de
vdeo
(GDDR5)
Cores Caches
50 GB/s.
3 canaless (192 bits = 24 bytes)
@ 1.333 GHz 32 GB/s.
PCI-e 3.0: 8 GB/s.
384 bits @ 3 GHz 144 GB/s.
Computacin heterognea (3/4)
13
#include <iostream>
#include <algorithm>
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;
// 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];
}
// 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
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);

// Alloc space for device copies
cudaMalloc((void **)&d_in, size);
cudaMalloc((void **)&d_out, size);
// Copy to device
cudaMemcpy(d_in, in, size, cudaMemcpyHostToDevice);
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
cudaMemcpy(out, d_out, size, cudaMemcpyDeviceToHost);
// Cleanup
free(in); free(out);
cudaFree(d_in); cudaFree(d_out);
return 0;
}
Computacin heterognea (4/4)
CODIGO DEL HOST:
- Cdigo serie.
- Cdigo paralelo.
- Cdigo serie.
CODIGO DEL DISPOSITIVO:
Funcin paralela
14
Un sencillo flujo de procesamiento (1/3)
1. Copiar los datos de entrada de
la memoria de la CPU a la
memoria de la GPU.
Bus PCI
15
Un sencillo flujo de procesamiento (2/3)
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.
PCI Bus
16
Un sencillo flujo de procesamiento (3/3)
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.
PCI Bus
17
El clsico ejemplo
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.
Salida:
$ nvcc hello.cu
$ a.out
Hola mundo!
$
18
int main(void) {
printf("Hola mundo!\n");
return 0;
}
Hola mundo! con cdigo para la GPU (1/2)
Dos nuevos elementos sintcticos:
La palabra clave de CUDA __global__
indica una funcin que se ejecuta en
la GPU y se lanza desde la CPU. Por
ejemplo, mikernel<<<1,1>>>.
Eso es todo lo que se requiere
para ejecutar una funcin en GPU.
19
__global__ void mikernel(void)
{
}
int main(void)
{
mikernel<<<1,1>>>();
printf("Hola mundo!\n");
return 0;
}
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 que corresponden a la CPU (como main()) son
procesadas por su compilador (como gcc para Unix o cl.exe para
Windows).
Hola mundo! con cdigo para la GPU (2/2)
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).
20
__global__ void mikernel(void) {
}
int main(void) {
mikernel<<<1,1>>>();
printf("Hola mundo!\n");
return 0;
}
Salida:
$ nvcc hello.cu
$ a.out
Hola mundo!
$
Computacin en GPU
NVIDIA GPU con la arquitectura
de computacin paralela CUDA
C OpenCL
tm Direct
Compute
Fortran Java y
Python
C++
Si tenemos una arquitectura CUDA, podemos
programarla de muy diversas formas...
... aunque este tutorial se focaliza sobre CUDA C.
21
La evolucin de CUDA
En los ltimos 5 aos, Nvidia ha vendido ms de 350 millones de
GPUs que aceptan CUDA para su programacin.
Pero CUDA ha evolucionado en la direccin opuesta a la que estamos
acostumbrados: Desde los investigadores hasta los usuarios ms
genricos.
22
Versin de
CUDA [ao]
Usuarios
1.0 [2007]
2.0 [2008]
3.0 [2009]
4.0 [2011]
5.0 [2012]
Muchos investigadores y algunos usuarios madrugadores.
Cientficos y aplicaciones para computacin de altas prestaciones.
Lderes en la innovacin de aplicaciones.
Adopcin mucho ms extensa de desarrolladores.
Optimizada para Kepler y disponible pblicamente en el tercer trimestre de 2012.
II. Arquitectura
23
II.1. El modelo hardware
de CUDA
24
Las generaciones hardware de CUDA
25
El modelo hardware de CUDA:
Un conjunto de procesadores SIMD
! La GPU consta de:
! N multiprocesadores, cada uno dotado
de M cores (o procesadores streaming).
! Paralelismo masivo:
! Aplicado sobre miles de hilos.
! Compartiendo datos a diferentes niveles
de una jerarqua de memoria.
! Computacin heterognea:
! GPU:
! Intensiva en datos.
! Paralelismo de grano fino.
! CPU:
! Gestin y control.
! Paralelismo de grano grueso.
26
GPU
Multiprocesador N
Multiprocesador 2
Multiprocesador 1
Unidad de
Control SIMD
Core 1

Core 2 Core M
G80
(Tesla)
GT200
(Tesla)
GF100
(Fermi)
GK110
(Kepler)
Marco temporal
N (multiprocs.)
M (cores/multip.)
Nmero de cores
2006-07 2008-09 2010-11 2012-13
16 30 14-16 13-15
8 8 32 192
128 240 448-512 2496-2880
Jerarqua de memoria
! Cada multiprocesador tiene:
! Su banco de registros.
! Memoria compartida.
! Una cach de constantes y otra
de texturas, ambas de slo
lectura y uso marginal.
! La memoria global es la
memoria de vdeo (GDDR5):
! Tres veces ms rpida que la
memoria principal de la CPU,
pero... 500 veces ms lenta
que la memoria compartida!
(que es SRAM en realidad).
27 13
GPU
Multiprocesador N
Multiprocesador 2
Multiprocesador 1
Memoria global
Memoria compartida
Unidad
de
Control
SIMD
Procesador 1
Registros
Procesador 2
Registros
Procesador M
Registros
Cach para
constantes
Cach para
texturas
28
Latencia y ancho de banda
de la memoria en CUDA
! Memoria de la CPU (o memoria principal - DDR3):
! Ancho de banda con mem. vdeo: 3.2 GB/s.(PCIe) y 5.2 GB/s(PCIe2).
! Memoria de vdeo (o memoria global):
! Gran ancho de banda (80-100 GB/s) y latencia, no pasa por cach.
! Memoria compartida
! Baja latencia, ancho de banda muy elevado, tamao reducido.
! Acta como una cach gestionada por el usuario (scratchpad).
! Memoria de texturas/constantes
! De slo lectura, alta/baja latencia, pasa por cach.

Host
CPU
Chipset
DRAM
Device
DRAM
Global
Constantes
Texturas
Local
GPU
Multiprocesador
Registros y
memoria compartida
Cachs para constantes
y texturas
II.2. La primera generation:
Tesla (G80 y GT200)
29
La arquitectura en general
! Se compone de multiprocesadores
dotados de 8 cores, donde los GFLOPS
escalan con el nmero de cores (stream
processors), y el ancho de banda escala
con el nmero de controladores de
memoria segn el modelo comercial:
30
Modelo GeForce GTS8600 GTX9800 GTX200
Multiprocesadores
Cores
GFLOPS
Control. de mem.
Anchura del bus
A. banda (GB/s)
4 16 30
32 128 240
93 429 624
2 4 8
64 128 256
32 70 141
Thread manager
cores
(stream
procs.)
Video
memory
GDDR3
>100 GB/s.
512 bits
TPC
(GTX200)
(GTX9800)
Evolucin segn el nmero de cores
! CCC = CUDA Compute Capabilities (describe el ritmo evolutivo).
! TPC = Thread Processing Cluster. El hardware del prop. general.
! SFU = Special Function Unit. Operadores matemticos complejos.
31
Modelo G80 G92 GT200 GF100
CCC
TPCs
SM/TPC
SMs
SP/SM
SPs
DP/SM
DPs
SFU/SM
SFUs
v1.0 v1.1 v1.3 v2.0
8 8 10 16
2 2 3 1
16 16 30 16
8 8 8 32
128 128 240 512
0 0 1 16
0 0 30 256
2 2 2 4
32 32 60 64
(GTS8600)
La primera generacin: G80 (GeForce 8800)
32
GPU G80 (en torno a 600 MHz, frecuencia muy inferior a la de sus cores)
Multiprocesador 16
Multiprocesador 2
Multiprocesador 1
Memoria global (hasta 1.5 GB) (GDDR3 @ 2x 800MHz)
Memoria compartida (16 KB)
Unidad de
control
(emite
instrucciones
SIMD)
Core 1
(1.35 GHz)
Registros

Core 2
Registros
Core 8
Registros
Cach de texturas
(los kernels se mapean
sobre los cores)
(los bloques de cdigo CUDA se mapean sobre los multipr.)
33
La primera generacin: GT200 (GTX 200)
GPU GTX 200 (en torno a 600 MHz)
Multiprocesador 30
Multiprocesador 2
Multiprocesador 1
Memoria global (hasta 4 GB) (GDDR3, 512 bits @ 2x 1.1GHz = 141.7 GB/s)
Memoria compartida (16 KB)
Unidad de
control
(emite
instrucciones
SIMD)
Core 1
(1.30 GHz)
Registros

Core 2
Registros
Core 8
Registros
Cach de texturas
(los kernels se mapean
sobre los cores)
(los bloques de cdigo CUDA se mapean sobre los multipr.)
34
Escalabilidad para futuras generaciones:
Alternativas para su crecimiento futuro
! Aumentar el nmero de
multiprocesadores por pares
(nodo bsico), esto es, crecer
en la dimensin Z. Es lo que
hizo la 1 gener. (de 16 a 30).
! Aumentar el nmero de
procesadores de cada
multiprocesador, o crecer en la
dimensin X. Es el camino
trazado por la 2 y 3 geners.
! Aumentar el tamao de la
memoria compartida, esto es,
crecer en la dimensin Y.
GPU
Multiprocesador 30
Multiprocesador 2
Multiprocesador 1
Memoria global
Memoria compartida
Core 1
Registros
Core 2
Registros
Core 8
Registros
Cach de texturas
(escalabilidad en 1 gener.)
(escalabilidad en 2 y 3 gener.)
II. 2. La segunda generacin:
Fermi (GF100)
35 36
El HW de Fermi comparado con los modelos
representativos de la generacin anterior
Arquitectura GPU
Nombre comercial
Ao de lanzamiento
G80 GT200 GF100 (Fermi)
GeForce 8800 GTX 200 GTX 580
2006 2008 2010
Nmero de transistores
Nmero de cores (int y fp32)
Nmero de cores (fp64)
Velocidad de clculo en fp64
Planificadores de warps
Shared memory
Cach L1
Cach L2
Correccin de errores
(DRAM)
Anchura del bus
de direcciones
681 millones 1400 millones 3000 millones
128 240 512
0 30 256
Ninguna 30 madds/ciclo 256 madds/ciclo
1 1 2
16 KB 16 KB 16 KB + 48 KB
(o viceversa)
Ninguna Ninguna
16 KB + 48 KB
(o viceversa)
Ninguna Ninguna 768 KB
No No S
32 bits 32 bits 64 bits
Fermi en perspectiva: Redimiento en FLOPS
y consumo energtico en vatios
37
Fabricante
Generacin arquit.
Nombre comercial
Intel nVidia nVidia nVidia
Nehalem T10P GT200 Fermi
x5550 Tesla C1060 GeForce GTX 285 Tesla C2050
Ciclo de reloj
Zcalo(s) de procesador
Cores por procesador
GFLOPS (simple prec.)
GFLOPS (doble prec.)
Ancho de banda (GB/s.)
Consumo del procesador
32 bit FLOPS / watt
64 bit FLOPS / watt
2.66 GHz 1.44 GHz 1.47 GHz 1.15 GHz
2 1 1 1
4 30 30 14
170 933 1060 1030
85 78 88 515
51 102 159 144
200 200 204 247
0,4265 0,3919 0,4313 2,0850
0,8530 4,665 5,1980 4,1700
Kepler ha acelerado esta tendencia hacia el bajo consumo.
Arquitectura global de Fermi
38
Hasta 512 cores (16 SMs dotados de 32 cores cada uno).
Los SPs crecen de 240 a 512, los DPs de 30 a 256 y los SFUs de 60 a 64.
Doble planificador de procesos (scheduler) para los hilos que entran a cada SM.
64 KB de SRAM, repartidos entre la cach L1 y la memoria compartida.
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):
! Implementa el formato IEEE-754 en su
versin de 2008, aventajando incluso a
las CPUs ms avezadas.
39
Load/Store Units x 16
Special Func Units x 4
FP Unit INT Unit
La jerarqua de memoria
40
13
! Fermi es la primera GPU que
ofrece una cach L1 tpica
on-chip, que combina con la
shared memory de CUDA en
proporcin 3:1 o 1:3 para un
total de 64 Kbytes por cada
multiprocesador (32 cores).
! Tambin incluye una cach
unificada de 768 Kbytes con
coherencia de datos para el
conjunto total de cores.
II. 3. La tercera generacin:
Kepler (GK110)
41
Diagrama de bloques: Kepler GK110
7.100 Mt.
15 SMX multiprocs.
> 1 TFLOP FP64.
1.5 MB L2 Cache.
384-bit GDDR5.
PCI Express Gen3.
42
Evolucin de los multiprocesadores:
Del SM de Femi al SMX de Kepler
43
Mejora de recursos en los SMX
44
Recurso
Kepler GK110
frente a Fermi GF100
Ritmo de clculo con opers. punto flotante
Mximo nmero de bloques por SMX
Mximo nmero de hilos por SMX
Ancho de banda del banco de registros
Capacidad del banco de registros
Ancho de banda de la memoria compartida
Capacidad de la memoria compartida
Ancho de banda de la cach L2
Capacidad de la cach L2
2-3x
2x
1.3x
2x
2x
2x
1x
2x
2x
SMX Balance of Resources
45
Resource Kepler GK110 vs. Fermi GF100
Floating-point throughput
Maximum number of blocks per SMX
Maximum number of threads per SMX
Register file bandwidth
Register file capacity
Shared memory bandwidth
Shared memory capacity
L2 bandwidth
L2 cache capacity
2-3x
2x
1.3x
2x
2x
2x
1x
2x
2x
Innovaciones de Kepler:
Paralelismo dinmico
46
CPU GPU CPU GPU
La GPU es un procesador autnomo La GPU es un co-procesador
Innovaciones de Kepler:
Paralelismo dinmico (2)
! El paralelismo puede depender de los datos, o incluso de la
potencia computacional asociada a las regiones de inters.
47
CUDA en 2012 CUDA sobre Kepler
III.Programacin
48
49
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.
! 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.
GPU
Multiprocesador 1 Multiprocesador 2 Multiprocesador N
! Debemos esperar que estas diferencias crezcan entre
modelos asociados a diferentes generaciones.
! Las diferencias tambin 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 tambin en sus prestaciones grficas,
aunque este tutorial no cubre las mismas.
50
Cada modelo se diferencia en pocos par-
metros para generar el catlogo comercial

51

Modelo comercial
Generacin
Ao
GTX 285 GTX 480 GTX 465 GTX 460
GT200 GF100 GF100 GF104
2009 2010 2010 2011
Precio de lanzamiento
Nmero de transistores
Proceso de fabricacin
Nmero de SMs
Cores en cada SM
Nmero total de cores
Reloj de los cores (MHz)
Tecnologa de memoria
Reloj de memoria (MHz)
Ancho del bus de mem.
Tamao de la memoria
! 300 ! 500 ! 250 ! 230/200
1400 mill. 3000 mill. 3000 mill. 1950 mill.
TSMC 55 nm. TSMC 40 nm. TSMC 40 nm TSMC 40 nm.
30 15 11 7
8 32 32 48
240 480 352 336
1476 1401 1215 1350
GDDR3 GDDR5 GDDR5 GDDR5
2x 1242 4x 924 4x 802 4x 900
512 bits 384 bits 256 bits 256 bits
1 GB. 1.5 GB. 1 GB. 1 GB. / 768 MB.
Ejemplo de catlogo comercial
52

Modelo comercial
Generacin
Ao
GTX 285 GTX 480 GTX 465 GTX 460
GT200 GF100 GF100 GF104
2009 2010 2010 2011
fp64 vs. fp32
Frecuencia GPU (MHz)
Direccionam. a texturas
Filtrado de texturas
Rasterizadores
1 vs. 16 1 vs. 8 1 vs. 8 1 vs. 6
648 700 607 675
80 60 44 56
80 60 44 56
32 48 32 32
Ejemplo de catlogo comercial (cont.)
53
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.
54
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.
Correspondencia entre los conceptos
hardware y software de CUDA
55
GPU
Multiprocesador N
Multiprocesador 2
Multiprocesador 1
Memoria global
Memoria compartida
Unidad
de
Control
SIMD
Procesador 1
Registros
Procesador 2
Registros
Procesador M
Registros
Cach para
constantes
Cach para
texturas
56
Recursos y limitaciones segn la GPU que
utilicemos para programar (CCC)
CUDA Compute Capability (CCC) CUDA Compute Capability (CCC) CUDA Compute Capability (CCC) CUDA Compute Capability (CCC)
Limitacin Impacto
1.0, 1.1 1.2, 1.3 2.0, 2.1 3.0, 3.5
Limitacin Impacto
Multiprocesadores / GPU
Cores / Multiprocesador
Hilos / Warp
Bloques / Multiprocesador
Hilos / Bloque
Hilos / Multiprocesador
Registros de 32 bits / Multiprocesador
Memoria compartida / Multiprocesador
16 30 14-16 14-16 Hardware
Escala-
bilidad
8 8 32 192 Hardware
Escala-
bilidad
32 32 32 32 Software Ritmo de
salida de
datos 8 8 8 16 Software
Ritmo de
salida de
datos
512 512 1024 1024 Software
Paralelismo
768 1 024 1 536 2048 Software
Paralelismo
8K 16K 32K 64K Hardware Conjunto
de
trabajo 16K 16K
16K
48K
16K,
32K, 48K
Hardware
Conjunto
de
trabajo

57
Bloques e hilos en GPU
Los bloques se
asignan a los
multiprocesadores
[Lmite en Kepler:
16 bloques
concurrentes por
cada
multiprocesador]
Bloque 0 Bloque 1 Bloque 2
Malla 0 [Lmite en Kepler: 4G bloques por malla]
Lmite en Kepler: 1024 hilos por bloque, 2048 hilos por multiprocesador
Los hilos se asignan a los multiprocesadores en bloques, que
constituyen la unidad para asignar hilos a cores via warps.
Los hilos de un bloque pueden compartir informacin a travs
de la memoria compartida, sincronizndose implcitamente (al
acabar el bloque) o explcitamente (con synchthreads()).
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.
58
Estos bloques
pueden
ejecutarse en
un mismo
multiprocesador
si se cumplen
ciertas
limitaciones de
espacio
Local memory: Off-chip
59
La memoria en la GPU: mbito y ubicacin

T
a
m
b
i

n

e
x
i
s
t
e

u
n
a

m
e
m
o
r
i
a

d
e

c
o
n
s
t
a
n
t
e
s

y

t
e
x
t
u
r
a
s
Bloque 0 Bloque 1 Bloque 2
Malla 0
Memoria global: GDDR5 (DRAM)
Memoria compartida
Los hilos pueden usar la memoria compartida para comunicarse entre
ellos dentro del bloque o realizar tareas de manera cooperativa y veloz.
La memoria global es la nica visible a todas las entidades definidas
para un programa CUDA (hilos, bloques, kernels).
BR BR BR BR BR BR BR BR
ML ML ML ML ML ML ML ML
Significado: BR: Banco de registros. ML = Memoria local
Memoria GPU: On-chip Off-chip
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.
60
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).
Pensando en pequeo: Particionamiento 1D
de un vector de 64 elementos
61
Pensando a lo grande: Particionamiento 1D
de un vector de 64 millones de elementos
Mximo nmero de hilos por bloque: 1K.
Maximo nmero de hilos por bloque:
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.
62
63
Recopilando sobre kernels,
bloques y paralelismo
! Los kernels se lanzan en grids.
! Slo se ejecuta un kernel a la vez.
! Un bloque se ejecuta en un
multiprocesador (SMX).
! El bloque no migra.
! Varios bloques pueden residir
concurrentemente en un SMX.
! Con las limitaciones de Kepler:
! Hasta 16 bloques concurrentes.
! Hasta 1024 hilos en cada bloque.
! Hasta 2048 hilos en cada SMX.
! Otras limitaciones entran en juego
debido al uso conjunto de la
memoria compartida y el banco de
registros segn hemos visto hace 3
diapositivas.
Malla
Bloque (0, 0)
Memoria compartida
Hilo (0, 0)
Regs Regs
Bloque (1, 0)
Memoria compartida
Hilo (0, 0)
Regs
Hilo (1, 0)
Regs
Memoria global
Hilo (1, 0)
64
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 dos SMX
SMX 0 SMX 1
Block 0 Block 1
Block 2 Block 3
Block 4 Block 5
Block 6 Block 7
Malla para el kernel
Block 0 Block 1
Block 2 Block 3
Block 4 Block 5
Block 6 Block 7
Device with 4 SMs
SMX 0 SMX 1 SMX 2 SMX 3
Block 0 Block 1 Block 2 Block 3
Block 4 Block 5 Block 6 Block 7
! Un kernel se puede expandir a lo largo de cualquier
nmero de procesadores (siempre que hayamos
declarado un nmero suficiente de bloques).
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.
65
IV. Sintaxis
66
IV. 1. Elementos bsicos
67
CUDA es C con algunas palabras clave ms.
Un ejemplo preliminar
68
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];
}
// Invocar al kernel SAXPY secuencial
saxpy_secuencial(n, 2.0, x, y);
__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);
Cdigo C estndar
Cdigo CUDA equivalente de ejecucin paralela en GPU:
Lista de extensiones sobre el lenguaje C
! Modificadores para las variables
(type qualifiers):
! global, device, shared, local, constant.
! Palabras clave (keywords):
! threadIdx, blockIdx.
! Funciones intrnsecas (intrinsics):
! __syncthreads
! API en tiempo de ejecucin:
! Memoria, smbolos, gestin de la
ejecucin.
! Funciones kernel para lanzar
cdigo a la GPU desde la CPU.
69
__device__ float vector[N];
__global__ void filtro (float *image) {
__shared__ float region[M];
...
region[threadIdx.x] = image[i];
__syncthreads()
...
imagen[j] = result;
}
// Alojar memoria en la GPU
void *myimage;
cudaMalloc(&myimage, bytes);
// 100 bloques de hilos, 10 hilos por bloque
convolucion <<<100, 10>>> (myimage);
70
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 ejecu-cin
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.
71
La interaccin entre la CPU y la GPU (cont.)
__global__ kernelA(){}
__global__ kernelB(){}
int main()

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

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

GPU
CPU
GPU
E
j
e
c
u
c
i

n
CPU
CPU
Un kernel no comienza hasta que terminan los anteriores.
Podemos emplear streams para definir kernels concurrentes.
Particin de datos para una matriz 2D [para
un patrn de acceso paralelo, ver ejemplo 2]
72
Anchura de la matriz
A
l
t
u
r
a

d
e

l
a

m
a
t
r
i
z
Malla (0, 0)
Bloque (0,0) Bloque (1,0) (blockDim.x,0)
(0,blockDim.y) (1,blockDim.y)
(blockDim.x,
blockDim.y)
Malla (gridDim.x, 0)
Block (0,0) Block (1,0) (blockDim.x,0)
(0,blockDim.y) (1,blockDim.y)
(blockDim.x,
blockDim.y)
Malla (0, gridDim.y)
Bloque (0,0) Bloque (1,0) (blockDim.x,0)
(0,blockDim.y) (1,blockDim.y)
(blockDim.x,
blockDim.y)
Malla (gridDim.x, gridDim.y)
Bloque (0,0) Bloque (1,0) (blockDim.x,0)
(0,blockDim.y) (1,blockDim.y)
(blockDim.x,
blockDim.y)
Dado que queremos
paralelismo de grado fino,
asignaremos un elemento a
cada hilo, que puede
conocer sus coordenadas
durante la ejecucin as:
- La posicin (hor,ver) para
el bloque dentro de la malla
es (blockIdx.x, blockIdx.y).
- La posicin (hor,ver) para
el hilo dentro del bloque es
(threadIdx.x, threadIdx.y)
Para este hilo:
- blockIdx.x is 1.
- blockIdx.y is 0.
- threadIdx.x is 11.
- threadIdx.y is 2.
73
Identificacin de los entes al programar
(sobre un ejemplo de suma de matrices)
! Para cada uno de los 4x4 bloques del grid:
! BlockIdx: vector (1D o 2D) que identifica el
bloque dentro del grid.
! Para cada uno de los 4x4 hilos del bloque:
! ThreadIdx: vector (1D, 2D o 3D) que identifica
el hilo dentro de su bloque.
Malla de
bloques
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
BlockDim.x
b
lo
c
k
D
im
.
y
__global__ void matAdd (oat A[N][N],oat B[N][N],oat C[N][N])
{
int i = blockIdx.x*blockDim.x + threadIdx.x;
int j = blockIdx.y*blockDim.y + threadIdx.y;
C[i][j] = A[i][j] + B[i][j];
{
int main(){
dim3 dimBlock(4,4);
dim3 dimGrid (N/dimBlock.x, N/dimBlock.y);
matAdd <<< dimGrid, dimBlock >>> (A, B, C);
}
Matrices A, B y C
de 16x16 elementos
blockIdx = (3,0)
threadIdx = (3,2)
Las tres matrices se
particionan igual,
otorgando un
elemento a cada hilo
Qu hilo computa C[15,2]?
BlockIdx.x
es 3
BlockIdx.y
es 0
74
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__ oat MyFunc() { } // Invocado por la GPU
! Modificadores para las variables que residen en la GPU:
! __shared__ oat MySharedArray[32]; // Mem. compartida
! __constant__ oat MyConstantArray[32];
! Configuracin de la ejecucin para lanzar kernels:
! dim2 gridDim(100,50); // 5000 bloques de hilos
! dim3 blockDim(4,8,8); // 256 bloques de hilos
! 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.
75
! dim3 gridDim; // Dimensin de la malla
! dim3 blockDim; // Dimensin del bloque
! uint3 blockIdx; // Indice del bloque dentro de la malla
! uint3 threadIdx; // Indice del hilo dentro del bloque
! void __syncthreads(); // Sincronizacin entre hilos
Variables y funciones intrnsecas
El programador debe elegir el tamao del bloque
y el nmero de bloques para explotar al mximo
el paralelismo del cdigo durante su ejecucin.
Funciones para conocer en tiempo de
ejecucin con qu recursos contamos
! Cada GPU disponible en la capa hardware recibe un nmero
entero consecutivo 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);
76
Ejemplo: Salida de la funcin
cudaGetDeviceProperties
! El programa se encuentra dentro del SDK de nVidia.
77
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:
! Para pasar los datos desde la CPU a la GPU:
! cudaMemcpy(d_A, h_A, numBytes, cudaMemcpyHostToDevice);
! Para 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 mem. de vdeo.
78
IV. 2. Un par de ejemplos
79
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.
80
Ejemplo 1: Implementacin
[cdigo C en rojo, extensiones CUDA en azul]
81
int main()
{
int N = 16;
int num_bytes = N*sizeof(int);
int *d_a=0, *h_a=0; // Punteros en device (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 alojar la 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);
}
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.
82
83
62
Programa C en CPU
(compilado con gcc)
El kernel CUDA que se ejecuta en la GPU
(parte superior), seguido del cdigo host
en CPU. Este archivo se compila con
nvcc (ver ms adelante).
void incremento_en_cpu(float *a, float b, int N)
{
for (int idx = 0; idx<N; idx++)
a[idx] = a[idx] + b;
}
void main()
{
.....
incremento_en_cpu(a, b, N);
}
__global__ void incremento_en_gpu(float *a, float b, int N)
{
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N)
a[idx] = a[idx] + b;
}
void main()
{
..
dim3 dimBlock (blocksize);
dim3 dimGrid( ceil( N / (float)blocksize) );
incremento_en_gpu<<<dimGrid, dimBlock>>>(a, b, N);
}
Ejemplo 2: Incrementar un valor b
a los N elementos de un vector
84
63
Con N = 16 y blockDim = 4, tenemos 4 bloques de hilos,
encargndose cada hilo de computar un elemento del vector.
blockIdx.x = 0
blockDim.x = 4
threadIdx.x = 0,1,2,3
idx = 0,1,2,3
blockIdx.x = 1
blockDim.x = 4
threadIdx.x = 0,1,2,3
idx = 4,5,6,7
blockIdx.x = 2
blockDim.x = 4
threadIdx.x = 0,1,2,3
idx = 8,9,10,11
blockIdx.x = 3
blockDim.x = 4
threadIdx.x = 0,1,2,3
idx = 12,13,14,15
int idx = (blockId.x * blockDim.x) + threadIdx.x;
Se mapear del ndice local threadIdx al ndice global
Nota: blockDim debera ser >= 32 (warp size) en cdigo real, esto es slo un ejemplo
Patrn de acceso
comn a todos
los hilos
E
x
t
e
n
s
i
o
n
e
s
a
l

l
e
n
g
u
a
j
e
Ejemplo 2: Incrementar un valor b
a los N elementos de un vector
85
64
// 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
incrementa_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);
Cdigo en CPU para el ejemplo 2
[rojo es C, gverde son vars., azul es CUDA]
V. Compilacin
86
El proceso global de compilacin
87
void funcion_en_CPU( )
{
...
}
void otras_funcs_CPU(int ...)
{
...
}
void saxpy_serial(float ... )
{
for (int i = 0; i < n; ++i)
y[i] = a*x[i] + y[i];
}
void main( ) {
float x;
saxpy_serial(..);
...
}
NVCC
(Open64)
Compilador
de la CPU
Kernels
CUDA
Ficheros objeto
CUDA
Resto del
cdigo C
Ficheros objeto
de la CPU
Enlazador
Ejecutable
CPU-GPU
Identificar
los kernels
CUDA y rees-
cribirlos para
aprovechar
paralelismo
en GPU
88
Los diferentes mdulos de compilacin
NVCC
C/C++ CUDA
Application
PTX to Target
Compiler
G80 GPU
PTX Code
Cdigo CPU
Cdigo
objeto
! El cdigo fuente CUDA se
compila con NVCC.
! NVCC separa el cdigo que
se ejecuta en CPU del que lo
hace en GPU.
! La compilacin se realiza
en dos etapas:
! Virtual: Genera cdigo PTX
(Parallel Thread eXecution).
! Fsica: Genera el binario para
una GPU especfica (o incluso
para una CPU multicore - ver
Ocelot 5 diap. ms adelante).
Cdigo
fuente
Virtual
Fsico
89
Compilador NVCC y mquina virtual PTX
! EDG
! Separa cdigo GPU y CPU.
! Open64
! Genera ensamblador PTX.
! Parallel Thread eXecution
(PTX)
! Mquina virtual e ISA.
! Modelo de programacin.
! Recursos y estado de ejecucin.
EDG
C/C++ CUDA
Application
CPU Code
Open64
PTX Code
ld.global.v4.f32 {$f1,$f3,$f5,$f7}, [$r9+0];
mad.f32 $f1, $f5, $f3, $f1;
float4 me = gx[gtid];
me.x += me.y * me.z;
NVCC (NVidia CUDA Compiler)
! NVCC es un driver del compilador.
! Invoca a los compiladores y herramientas, como cudacc, g++, cl, ...
! NVCC produce como salida:
! Cdigo C para la CPU, que debe luego
compilarse con el resto de la aplicacin
utilizando otra herramienta.
! Cdigo objeto PTX para la GPU.
90
Proceso de compilacin Linux:
Proceso de
compilacin
en Windows:
91
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}
modname {cubin}
code {
name = myGPUcode
lmem = 0
smem = 68
reg = 20
bar = 0
bincode {
0xa0004205 0x04200780 0x40024c09 0x00200780

Memoria local para cada hilo
(usada por el compilador para
volcar contenidos de los registros
en memoria)
Memoria compartida usada
por cada bloque de hilos
Registros usados
por cada hilo
92
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.
93
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.
94
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).
95
CUDA Occupancy Calculator
Asesora en la seleccin de los parmetros de configuracin
http://developer.download.nvidia.com/compute/cuda/CUDA_Occupancy_calculator.xls
Para alcanzar el mayor grado de paralelismo,
fijarse en la tabla naranja del CUDA Occup.
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.
96
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 8 bloques por SMX (2048 threads/SMX):
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.
97
Para alcanzar el mayor grado de paralelismo,
fijarse en el rea naranja del CUDA Occup.
! 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 (CCC 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.
98
99
Vdeo ilustrativo del uso del
CUDA Occupancy Calculator
VI. Una serie de tres ejemplos:
VectorAdd, Stencil, MxM
100
101
Pasos para la construccin
del cdigo fuente 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.
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:
Paso 1.
__syncthreads();
Paso 2.
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(); 102
VI. 1. Suma de dos vectores
103
a b c
Cdigo para GPU y su invocacin desde 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. 104
// 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(oat* A, oat* B, oat* 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
105 64
unsigned int numBytes = N * sizeof(float);
// Aloja memoria en la CPU
float* h_A = (float*) malloc(numBytes);
float* h_B = (float*) malloc(numBytes);
... initializes h_A and 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);
Cdigo CPU para manejar la memoria y
recoger los resultados de GPU
Ejecutando en paralelo
(independientemente de la generacin HW)
vecAdd<<< 1, 1 >>>():
Ejecuta 1 bloque de 1 hilo.
No hay paralelismo.
vecAdd<<< N, 1 >>>():
Ejecuta N bloques de 1 hilo.
Hay paralelismo entre
multiprocesadores.
vecAdd<<< N, M >>>():
Ejecuta N bloques compuestos
de M hilos. Hay paralelismo
entre multiprocesadores y entre
los cores del multiprocesador.
106
GPU
Multiprocesador 30
Multiprocesador 2
Multiprocesador 1
Memoria global
Memoria compartida
Core 1
Registros
Core 2
Registros
Core 8
Registros
Cach de texturas
(escalabilidad en 1 gener.)
(escalabilidad en 2 y 3 gener.)
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), N=4
bloques de M=8 hilos cada uno:
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).
Calculando el ndice de acceso a un vector
segn el bloque y el hilo dentro del mismo
107
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6
threadIdx.x threadIdx.x threadIdx.x threadIdx.x
blockIdx.x = 0 blockIdx.x = 1 blockIdx.x = 2 blockIdx.x = 3
7
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:
Y ahora, hay que incluir el bloque "incompleto" de hilos
en el lanzamiento del kernel (redondeo al alza en la div.):
108
// Suma dos vectores de tamao N: C[1..N] = A[1..N] + B[1..N]
__global__ void vecAdd(oat* A, oat* B, oat* C, N) {
int tid = threadIdx.x + (blockDim.x * blockIdx.x);
if (tid < N)
C[tid] = A[tid] + B[tid];
}
vecAdd<<< (N + M-1)/256, 256>>>(d_A, d_B, d_C, N);
VI. 2. Kernels patrn (stencils)
109
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 respetar las dependencias de
datos).
Para ilustrarlo, necesitamos un ejemplo ms sofisticado ...
110
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:
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.
111
radio radio
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 prefigo __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. 112
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. 113
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.
114
blockDim.x elementos de salida
extensin por
la izquierda
extensin por
la derecha
Kernel patrn
115
__global__ void stencil_1d(int *in, int *out) {
__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] = in[gindex];
if (threadIdx.x < RADIO) {
temp[lindex-RADIO] = in[gindex-RADIO];
temp[lindex+BLOCK_SIZE] = in[gindex+BLOCK_SIZE];
}
// Aplicar el patrn
int result = 0;
for (int offset=-RADIO; offset<=RADIO; offset++)
result += temp[lindex + offset];
// Almacenar el resultado
out[gindex] = result;
}
Pero hay que prevenir
condiciones de carrera. Por
ejemplo, el ltimo hilo (15)
lee la extensin antes del que
el primer hilo (0) haya
cargado esos valores.
Es necesaria una
sincronizacin entre hilos.
Sincronizacin entre hilos
Usar __synchthreads();
para sincronizar todos los hilos
de un bloque:
La barrera tiene que ser alcanzada
por todos los hilos antes de que
stos puedan continuar.
Puede utilizarse para prevenir
riesgos del tipo RAW / WAR / WAW.
En sentencias condicionales, la
codicin debe ser uniforme a lo
largo de todo el bloque.
116
__global__ void stencil_1d(...) {
< Declarar variables e ndices>
< Pasar el vector a mem. compartida >

__synchthreads();
< Aplicar el patrn >
< Almacenar el resultado >
}
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();
117
VI. 3. Invertir el orden a
los elementos de un vector
118
Cdigo en GPU para el kernel ReverseArray
(1) utilizando un nico bloque
119
__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.
Cdigo en GPU para el kernel ReverseArray
(2) con mltiples bloques
120
__global__ void reverseArray(int *in, int *out) {
int in_offset = blockIdx.x * blockDim.x;
int out_offset = blockDim.x * (gridDim.x 1 blockIdx.x);
int index_in = threadIdx.x + in_offset;
int index_out = out_offset + (blockDim.x 1 threadIdx.x);
// Invertir los contenidos en fragmentos de bloques enteros
out[index_out] = in[index_in];
}
Utilizando la memoria compartida para
acelerar el proceso
121
Cdigo en GPU para el kernel ReverseArray
(3) con mltiples bloques y mem. compartida
122
__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];
// Invertir los elementos del vector local a cada bloque
temp[lindex] = temp[blockDim.x-lindex-1];
// Invertir los contenidos en fragmentos de bloques enteros
out[threadIdx.x + ((N/blockDim.x)-blockIdx.x-1) * blockDim.x] = temp[lindex];
}
VI. 4. Producto de matrices
123
Versin de cdigo CPU escrita en lenguaje C
C = A * B.
Matrices cuadradas de tamao N * N.
Linearizadas en vectores para simplificar
el alojamiento de memoria dinmica.
124
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 = A[i*N + k];
float b = B[k*N + j];
sum += a*b;
}
C[i*N + j] = sum;
}
}
A
B
C
N
N N
Versin CUDA para el producto de matrices:
Un primer borrador para el cdigo paralelo
125
__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;
}
C[i*N + j] = sum;
}
A
B
C
N
N N
126
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
(limitacin interna en arquitecturas Fermi y Kepler).
As podemos usar bloques 2D de 32x32 hilos cada uno.
N
N
N N
X =
C(x, y)
Anchura A
A
l
t
u
r
a

A
Anchura B
A
l
t
u
r
a

A
Anchura B
C A B
! ! ! ! ! ! ! ! ! ! ! ! ! ! !
! ! ! ! ! ! !






! ! ! ! ! ! !






Malla
Bloque
Hilo(x,y)
dim2 dimBlock(BLOCKSIZE, BLOCKSIZE);
dim2 dimGrid(AnchuraB/BLOCKSIZE, AlturaA/BLOCKSIZE);
...
MxMonGPU <<<dimGrid,dimBlock>>> (A, B, C, N);
A
l
t
u
r
a

A
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 registros 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.
127 128
La submatriz de C
sub
de 32x32 datos
computada por cada bloque de hilos
utiliza mosaicos de 32x32 elementos de
A y B que se alojan de forma reiterativa
en memoria compartida.
A y B se cargan slo (N/32) veces
desde memoria global.
Logros:
Menos exigente en el
ancho de banda a memoria.
Ms intensidad aritmtica.
A
B
C
C
sub
M M M M
M
M
M
M
N
N
N N
Utilizando la memoria compartida:
Versin con mosaicos (tiling)
Tiling: Detalles de la implementacin
Tenemos que gestionar todos los mosaicos pertenecientes a
cada bloque de hilos:
Cargar en paralelo (contribuyen todos los hilos) los mosaicos de entrada
(A y B) desde memoria global a memoria compartida. 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 eficientemente sobre los elementos del mosaico.
__syncthreads() (para asegurarnos que la computacin con el
mosaico ha acabado antes de cargar dos nuevos mosaicos para A y B en la
siguiente iteracin).
Transferir los resultados de C alojamos en memoria global.
129
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.
130
Tiling: El cdigo CUDA para el kernel en GPU
131
__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;
}
132
Una optimizacin gracias al compilador:
Desenrrollado de bucles (loop unrolling)
Sin desenrrollar el bucle: Desenrrollando el bucle:
...
__syncthreads();
// Computar la parte de ese mosaico
for (k=0; k<32; k++)
sum += As[tx][k]*Bs[k][ty];
__syncthreads();
}
C[indexC] = sum;
...
__syncthreads();
// Computar la parte de ese mosaico
sum += As[tx][0]*Bs[0][ty];
sum += As[tx][1]*Bs[1][ty];
sum += As[tx][2]*Bs[2][ty];
sum += As[tx][3]*Bs[3][ty];
sum += As[tx][4]*Bs[4][ty];
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;
133
Rendimiento con tiling & unrolling en la G80
0
25
50
75
100
G
F
L
O
P
S
4x4 8x8 12x12 16x16
Tamao del mosaico (32x32 no es factible en la G80)
Slo tiling
Tiling & Unrolling

También podría gustarte