CUDA I. Introducción - CAPAP-H

Indice de contenidos
1. Introducción. [11 diapositivas]
2. Arquitectura. [25]
CUDA
1. El modelo hardware de CUDA. [6]
2. Las dos primeras generaciones (2006-2009) [7]
3. La tercera generación: Fermi (2010). [12]
Manuel Ujaldón Martínez
3. Programación. [20]
4. Sintaxis. [17]
5. Compilación. [4]
6. Depuración y optimización. [12]
7. Estrategias de mejora. [7]
8. Ejemplos. [3+12+8]
Departamento de Arquitectura de Computadores
Universidad de Málaga
El camino hacia los sombreadores
unificados que dieron lugar a CUDA
! Cuestión del año 2000: La programación de video-juegos
requiere instrumentos para recrear la riqueza de
materiales y efectos del mundo real.
! Respuesta del año 2002: Aparecen los sombreadores para
vértices y píxeles.
! Cuestión del año 2005: Algunas etapas del cauce de
segmentación gráfico son programables, pero con fuertes
restricciones:
I. Introducción
! En cómo pueden comunicarse entre ellas.
! En cómo pueden acceder a memoria global.
! Respuesta del año 2007: Se unifican los sombreadores, y
sobre ellos, surge el modelo de programación CUDA.
3
4
Alternativas a CUDA para
la programación paralela en GPUs
De menor a mayor afinidad durante esta década:
! Fujitsu’s: Llamadas a procedimientos remotos.
! Brook: El compilador pionero de Stanford.
! PeakStream: Librerías matemáticas para Radeon.
! RapidMind: Directivas de más alto nivel y una generación
de código más automatizada. Genera código para CPUs
x86, GPUs y Cell.
! Tilera’s: Hardware con topología malla escalable a 512
procesadores e incluso más.
! CAL (Compute Abstraction Layer) y CTM: Variantes AMD.
! OpenCL: El nuevo estándar, superconjunto de CUDA.
Los paradigmas de computación
hasta llegar a CUDA
5
¿Qué es CUDA?
“Compute Unified Device Architecture”
6
CUDA: Relación con la CPU
! CUDA ejecuta un programa sobre un device aparte (la
GPU), que actúa como coprocesador de un host (la CPU).
! Una plataforma diseñada conjuntamente a nivel software y
hardware para aprovechar la potencia de una GPU en
aplicaciones de propósito general.
! A nivel software, permite programar la GPU en C con mínimas
pero potentes extensiones SIMD para lograr una ejecución
eficiente y escalable.
! A nivel firmware, ofrece un driver para la programación
GPGPU que es compatible con el que se usa para renderizar.
! A nivel hardware, habilita múltiples niveles de paralelismo.
Según las 3 generaciones ya fabricadas, tenemos (1ª-2ª-3ª):
! Extensión de C.
! Biblioteca de funciones con tres tipos de componentes:
! Host: Control y acceso a los devices.
! Device: Funciones específicas de los devices.
! Comunes: Tipos vectoriales y un conjunto de rutinas soportadas por host y device.
CPU
HOST
! Multiprocesadores de 16-30-16 nodos sobre los que se ejecutan bloques.
! Nodos de 8-8-32 procesadores (SPs) sobre los que se ejecutan hilos.
! Jerarquía de memoria: Registros, memoria compartida y global (DRAM).
Memoria
principal
7
E/S
(PCIe)
GPU
DEVICE
Memoria de
vídeo
8
Ventajas de CUDA sobre la programación
GPGPU precursora (Cg y OpenGL)
Objetivos de CUDA
! Habilitar paralelismo masivo en GPU ejecutando
programas SIMD sin las limitaciones y sobrecargas del
API gráfico. GPGPU ya no es código OpenGL.
! Permitir al programador involucrarse a distintos grados
de exigencia según el rendimiento:
! Memoria direccionable a nivel de byte.
! Un hilo puede acceder a cualquier posición de memoria.
! Acceso ilimitado a memoria.
! Un hilo puede leer/escribir en tantas posiciones de memoria como
sea necesario.
! Básico: Posibilita una fácil portabilidad desde C++.
! Medio: Requiere un buen conocimiento de la arquitectura
gráfica.
! Avanzado: Permite un mapeo eficiente del problema sobre
muchos procesadores minimizando conflictos en el acceso a
memoria.
! Sincronización para los hilos y la memoria compartida
(para cada bloque).
! Los hilos pueden cargar datos en memoria compartida de forma
cooperativa dentro de un bloque.
9
47
10
Esfuerzo del programador CUDA
en relación al rendimiento esperado
Ventajas de CUDA sobre la programación
GPGPU precursora (2)
! La fase de aprendizaje resulta muy corta, ya que los
elementos a utilizar resultan familiares:
! Código: Apenas unas pocas extensiones al C convencional.
! Datos: No se requiere un conocimiento de gráficos (vértices,
texturas, píxeles).
! El tiempo de ejecución del código no sufre una
penalización por la sobrecarga del API gráfico (DirectX/
OpenGL).
! La depuración y optimización de código resultan mucho
más llevaderas.
48
11
Fuente: Curso de CUDA impartido en el MIT.
12
Si programamos con CUDA, la arquitectura
destino no tiene por qué ser CUDA
Si instalamos una arquitectura CUDA, la
programación tampoco tiene que ser CUDA
! A partir de CUDA 2.1 (2009), podemos compilar
indistintamente para many-core GPU o multi-core CPU
! Se soporta Windows, Linux y MacOS, y sobre ellos,
herramientas de desarrollo y lenguajes de programación
plenamente consolidados:
! Y con OpenCL, el abanico de plataformas es más amplio.
13
14
II.1. El modelo hardware
de CUDA
II. Arquitectura
15
16
El modelo hardware de CUDA:
Un conjunto de procesadores SIMD
Arquitectura del sistema de memoria
! La GPU consta de:
! Cada multiprocesador tiene:
GPU
! N multiprocesadores, cada uno
dotado de M procesadores.
Multiprocesador N
! Su banco de registros.
! Memoria compartida.
! Una caché de constantes y otra
de texturas, ambas de sólo
lectura y uso marginal.
Multiprocesador 2
Multiprocesador 1
! Paralelismo masivo:
Procesador 1
! Aplicado sobre miles de hilos.
! Compartiendo datos a
diferentes niveles.
Procesador 2
…
G80
Período de
vigencia
! Computación heterogénea,
complementa CPU y GPU:
! GPU: Intensiva en datos.
Paralelismo de grano fino.
! CPU: Gestión y control.
Paralelismo de grano grueso.
ProcesadorM
Unidad de
Control
2006-07 2008-09 2010-11
N
16
30
16
M
8
8
32
128
240
512
Número total
de cores
! La memoria global es la
memoria de vídeo (DRAM):
GTX200 Fermi
17
! Tres veces más rápida que la
de la CPU, pero...
! ... ¡500 veces más lenta que la
memoria compartida! (que es
SRAM en realidad).
GPU
Multiprocesador N
Multiprocesador 2
Multiprocesador 1
Memoria compartida
Registros
Registros
Procesador 1
Procesador 2
Registros
…
Procesador M
Unidad
de
Control
Caché para
constantes
Caché para
texturas
Memoria global
Latencia y ancho de banda
de la memoria en CUDA
Arquitectura de cada grupo o nodo básico
para la construcción de las distintas GPUs
! Memoria de la CPU
! Se compone de un par de multiprocesadores de ocho
cores o SPs (stream processors). Cada multiprocesador
dispone de una memoria compartida o caché de 16 KB
casi tan rápida como el banco de registros, y por la que
pueden comunicarse los hilos pertenecientes al mismo.
! Computación de punto flotante:
! Ancho de banda con mem. vídeo: 3.2 GB/s.(PCIe) y 5.2 GB/s(PCIe2).
! Memoria de vídeo global/local
! Gran ancho de banda (80-100 GB/s) y latencia, no pasa por caché.
! Memoria compartida
! Baja latencia, ancho de banda muy elevado, tamaño reducido.
! Actúa como una caché gestionada por el usuario (scratchpad).
! Memoria de texturas/constantes
! Para 32 bits (2006).
! Para 64 bits (2008).
! De sólo lectura, alta/baja latencia, pasa por caché, uso marginal.
Host
Device
CPU
Chipset
DRAM
Local
Global
Constantes
DRAM
Texturas
13
18
GPU
Multiprocesador
Registros y
memoria compartida
Cachés para constantes
y texturas
19
20
Escalabilidad del modelo
aumentando el número de nodos
Escalabilidad del modelo: En 8 y 15 nodos
llegamos a las dos primeras generaciones
Dos nodos: GeForce 8400
! El modelo de 8 nodos es el de la GPU G80 (1ª gener.).
Cuatro nodos: GeForce 8600
! El modelo de 15 nodos es el de la GTX200 (2ª gener.).
21
22
La arquitectura en general
II.2. Las dos primeras
generaciones
! Se compone de multiprocesadores de 8 cores, donde los
GFLOPS escalan con el número de cores (stream processors),
y el ancho de banda escala con el número de controladores
de memoria según el modelo comercial:
32, 128 ó 240 cores
Gestor de hilos
GDDR3
>100 GB/sg.
Número de
multiprocesadores
512 bit
Memoria
de vídeo
23
8600 9800 GTX
GTS GTX 200
4
16
30
93
429
624
Controladores de
memoria
2
4
8
Ancho de banda
(GB/s)
32
70
141
GFLOPS
24
Detalle de cada multiprocesador:
Sus unidades funcionales
! Ocho ALUs (una en cada
procesador escalar) para
operaciones add/mul, tanto
para enteros como para p.f.
! Dos SFUs para operadores
complejos (sin, cos, inv, sqrt).
! Una unidad p.f. doble precisión.
! Rendimiento pico en cada multiprocesador:
Inestabilidades numéricas
(ya han sido resueltas en Fermi)
Búsqueda de instrucción
Planif. instr.
Format
Registros
Rounding modes for
FADD and FMUL
Memoria compartida
ALU
Denormal handling
ALU
ALU
NaN support
ALU
ALU
Overflow and
infinity support
ALU
SFU
ALU
SFU
Unidad de punto flotante
en doble precisión
ALU
Flags
! En la G80: 18 ops. por ciclo (8 mul/add flotante y 2 ops complejas):
! Para 16 multiprocesadores a 1.35 GHz: 16 x 18 x 1.35 = 388.8 GFLOPS
! En la GTX200: 24 ops. por ciclo (1 mul/add flotante y 1 mul):
! Para 30 multiprocesadores a 1.296 GHz: 30 x 24 x 1.296 = 933 GFLOPS
25
Detalle de cada procesador o SP
G80
SSE
IBM Altivec
Cell SPE
IEEE 754
IEEE 754
IEEE 754
IEEE 754
Round to nearest and
round to zero
All 4 IEEE, round to
nearest, zero, inf, -inf
Round to nearest only
Round to zero/truncate
only
Flush to zero
Supported,
1000’s of cycles
Supported,
1000’s of cycles
Flush to zero
Yes
Yes
Yes
No
Yes, only clamps to
max norm
Yes
Yes
No, infinity
No
Yes
Yes
Some
Square root
Software only
Hardware
Software only
Software only
Division
Software only
Hardware
Software only
Software only
Reciprocal estimate
accuracy
24 bit
12 bit
12 bit
12 bit
Reciprocal sqrt
estimate accuracy
23 bit
12 bit
12 bit
12 bit
log2(x) and 2^x
estimates accuracy
23 bit
No
12 bit
No
26
La primera generación: G80 (GeForce 8800)
! Se trata de unidades vectoriales multihilo:
GPU G80 (en torno a 600 MHz, frecuencia muy inferior a la de sus cores)
Multiprocesador 16
La emisión múltiple de
instrucciones se consigue
ejecutando más veloz el
front-end (CPI=2) que el
back-end (CPI = 4,16,32).
Multiprocesador 2
Multiprocesador 1 (los bloques de código CUDA se mapean sobre los multipr.)
Memoria compartida (16 KB)
Registros
Core 1
Esta idea recuerda al
hyperthreading de Intel.
(1.35 GHz)
Registros
Core 2
…
Caché de texturas
Registros
Core 8
Unidad de
control
(emite
instrucciones
SIMD)
(los kernels se mapean
sobre los cores)
Memoria global (hasta 1.5 GB) (GDDR3 @ 2x 800MHz)
27
28
Escalabilidad para futuras generaciones:
Alternativas para su crecimiento futuro
La segunda generación (GTX200)
GPU GTX200 (en torno a 600 MHz)
Multiprocesador 30
Multiprocesador 2
Multiprocesador 1 (los bloques de código CUDA se mapean sobre los multipr.)
Memoria compartida (16 KB)
Registros
Core 1
(1.30 GHz)
Registros
Core 2
…
Caché de texturas
Registros
Core 8
Unidad de
control
(emite
instrucciones
SIMD)
(los kernels se mapean
sobre los cores)
Memoria global (hasta 4 GB) (GDDR3, 512 bits @ 2x 1.1GHz = 141.7 GB/s)
29
GPU
! Aumentar el número de
Multiprocesador 30
multiprocesadores por pares
(escalabilidad en 2ª gener.)
(nodo básico), esto es, crecer
Multiprocesador 2
en la dimensión Z. Es lo que
Multiprocesador 1
hizo la 2ª gener. (de 16 a 30).
Memoria compartida
! Aumentar el número de
Registros
Registros
Registros
procesadores de cada
Core 2 … Core 8
Core 1
multiprocesador, o crecer en la
(escalabilidad en 3ª gener.)
dimensión X. Es lo que hizo la
Caché de texturas
3ª gener. (de 8 a 32).
! Aumentar el tamaño de la
memoria compartida, esto es,
Memoria global
crecer en la dimensión Y.
30
La nueva GPU en cifras, y respecto
a sus precedesores en la saga GeForce
II. 3. La tercera generación:
Fermi
G80 (2006)
GTX200 (2008)
Fermi (2010)
Transistores
681 millones
1400 millones
3000 millones
Cores CUDA
128
240
512
Vel. cálculo en
doble precisión
Ninguna
30 madds/clock
256 madds/clock
Planificadores
de warps
1
1
2
Shared memory
31
16 KB
16 KB
Caché L1
Ninguna
Ninguna
16 KB + 48 KB
(o vice versa)
Caché L2
Ninguna
Ninguna
768 KB
Corrección de
errores (DRAM)
No
No
Sí
Anchura del bus
de direcciones
32 bits
32 bits
64 bits
32
DRAM
I/F
!
- 3000 millones de transistores.
!
- Más del doble de cores que en la
generación anterior (512 vs. 240).
!
- 8 veces más de rendimiento pico
sobre punto flotante de doble
precisión que la generación anterior.
A 2 GHz, proporciona 1 TFLOPS.
!
- Corrección de errores (ECC).
!
- Cachés L1 y L2 transparentes.
!
- El doble de ancho de banda con
memoria de vídeo (GDDR5).
!
- Hasta 1 Terabyte de memoria de
vídeo.
!
- Kernels concurrentes, C++.
L2
DRAM
I/F
DRAM
I/F
DRAM Giga ThreadHOST I/
I/F
DRAM
I/F
DRAM
I/F
Principales rasgos de Fermi
“The soul of a Supercomputer in the body of a GPU”
La disposición física de los
cores y la memoria apenas
ha cambiado respecto a su
predecesor GTX200.
Arquitectura global de Fermi
33
34
Arquitectura de los multiprocesadores
! Mantener la escalabilidad de la GPU en años venideros.
! Atraer a un mayor número de usuarios y aplicaciones.
! Se dispone de 16 multiprocesadores,
cada uno compuesto de 32 cores.
! Gran mejora de la computación de
punto flotante en doble precisión:
L2
! Aumenta en un factor 8x su
rendimiento pico vs. G80.
! Tan sólo computa el doble de
lento que en simple precisión.
DRAM I/F
DRAM I/F
DRAM I/F
DRAM I/F Giga Thread HOST I/F
DRAM I/F
DRAM I/F
Objetivo de Fermi
! Dos planificadores de threads.
! 64 KB. de SRAM que se reparten entre
la memoria compartida y la caché L1.
35
Load/Store Units x 16
Special Func Units x 4
36
Arquitectura de los cores
La jerarquía de memoria
! Implementa el formato IEEE-754 en su
versión de 2008, aventajando incluso
a las CPUs más avezadas.
! Instrucción “madd” (suma
y producto simultáneos)
disponible tanto para simple
como para doble precisión.
FP Unit
! ALU rediseñada para optimizar
las operaciones sobre enteros
de 64 bits y admitir operaciones
de precisión extendida.
! La primera GPU que ofrece
una caché L1 típica on-chip,
que combina con la shared
memory de CUDA para un
total de 64 KB por cada
multiprocesador (32 cores).
! También incluye una caché
unificada de 768 KB con
coherencia de datos para el
conjunto de cores.
INT Unit
Load/Store Units x 16
Special Func Units x 4
13
37
38
Planificador hardware de threads GigaThread
(HTS)
El planificador de instrucción
! Gestiona jerárquicamente miles de threads
simultáneamente activos.
! El cambio de contexto de la aplicación es diez veces más
rápido.
HTS
14
39
40
Planificador hardware de threads GigaThread
(HTS)
GigaThread: El motor de transferencia
de datos streaming GigaThread (SDT)
! Dos coprocesadores de DMA:
! Gestiona jerárquicamente miles de
threads simultáneamente activos.
SDT
! Ejemplo de actividad:
!
Kernel 0
SDT0
Kernel 1
SDT1
SDT0
Kernel 2
SDT1
SDT0
Kernel 3
SDT1
SDT0
SDT1
41
42
El entorno de desarrollo Nexus: Integrando
toda la programación en MS Visual Studio
III. Programación
15
43
44
El ecosistema corporativo y funcional
de CUDA en su vertiente software
El modelo de programación CUDA
! La GPU (device) ofrece a la CPU (host) la visión de un
coprocesador altamente ramificado en hilos.
! Que tiene su propia memoria DRAM.
! Donde los hilos se ejecutan en paralelo sobre los núcleos (cores o
stream processors) de un multiprocesador.
GPU
Multiprocesador 1
Multiprocesador 2
Multiprocesador N
! Los hilos de CUDA son extremadamente ligeros.
Oil & Gas
Finance
CFD
Medical
Biophysics
Imaging
Numerics
DSP
! Se crean en un tiempo muy efímero.
! La conmutación de contexto es inmediata.
ANEO
! Objetivo del programador: Declarar miles de hilos, que
la GPU necesita para lograr rendimiento y escalabilidad.
GPU Tech
EDA
45
46
!
Ejemplo: GeForce 8 y Quadro FX
Estructura de un programa CUDA
! El número de procesadores, su velocidad y la memoria
global disponibles depende del modelo comercial que
suscribamos.
! Cada multiprocesador procesa lotes de bloques, uno
detrás de otro
Número de
multiprocesadores
Frecuencia de
reloj (GHz)
Memoria de vídeo
(MB)
GeForce 8800 GTX
16
1.35
768
GeForce 8800 GTS
12
1.2
640
Quadro FX 5600
16
1.35
1500
Quadro FX 4600
12
1.2
768
Modelo de GPU
! 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 multiprocesador se reparten entre sus hilos activos. Para un
kernel dado, el número de bloques activos depende de:
! El número de registros requeridos por el kernel.
! La memoria compartida consumida por el kernel.
47
48
Recursos y limitaciones según la GPU
que utilicemos para programar CUDA
Conceptos básicos
!
!
!
!
!
Los programadores se enfrentan al reto de exponer el
paralelismo para múltiples cores y para múltiples hilos por
core. Para ello, deben usar los siguientes elementos:
Device = GPU = conjunto de multiprocesadores.
Multiprocesador = conjunto de procesadores y memoria
compartida.
Kernel = programa ejecutándose en GPU.
Grid = matriz de bloques de hilos que ejecutan un kernel.
Bloque de hilos (thread block) = grupo de hilos SIMD que
ejecutan un kernel delimitando su dominio computacional
según su threadID, y que pueden comunicarse a través de la
memoria compartida del multiprocesador.
Parámetro
CUDA Compute Capabilities
Máximo
8 bloques
!
····
Máximo
512 hilos
····
· · · · ·· ·· · ·
····
Asignación
a un multiproc.
HW.
Escalabilidad
8
8
32
HW.
Escalabilidad
32
32
32
SW.
Throughput
8
8
8
SW.
Throughput
Hilos / Bloque
512
512
512
SW.
Paralelismo
Hilos / Multiprocesador
768
1 024 1 536
SW.
Paralelismo
Registros de 32 bits / Multiprocesador
8 192
16 384 4 096
HW.
Working set
Memoria compartida / Multiprocesador
16 384
16K
48K
HW.
Working set
SFU
Core
Core
Core
Core
SFU
Máximo 768, 1024 ó 1536
hilos según la gener. de GPU
! Los hilos se asignan a los multiprocesadores en “bloques”,
que constituyen la unidad de asignación de hilos.
! Cada multiprocesador puede tener hasta 8 bloques y cada
bloque hasta 512 hilos. En total, un máximo de 768, 1024 o
1536 hilos pueden asignarse a cada multiprocesador.
! Los hilos de un bloque comparten información a través de
memoria compartida, y se sincronizan mediante barreras.
CPU (host)
...
32 hilos
=
Multiproc.
32 hilos
32 hilos
Bloque de hilos
51
GPU (device)
Grid 1
Bloque
(0, 0)
Bloque
(1, 0)
Bloque
(2, 0)
Bloque
(0, 1)
Bloque
(1, 1)
Bloque
(2, 1)
Grid 2
Kernel 2
Bloque (1, 0)
Hilo
(0, 0)
Hilo
(0, 1)
Hilo
(0, 2)
Warps
16
50
Kernel 1
! Cada bloque activo se descompone
en WARPs o grupos de 32 hilos con
ID correlativo que se ejecutan
usando tiempo compartido en un
multiprocesador.
! Los hilos de un WARP se
ejecutan físicamente en paralelo.
! Los WARPs y los bloques se
ejecutan lógicamente en paralelo.
Core
Core
16 384
49
WARPs. Concepto
Core
Core
16
Bloques de hilos / Multiprocesador
Memoria compartida
Impacto
30
Hilos / Warp
Registros
Limitación
16
Procesadores / Multiprocesador
B1
!!
1.0 y 1.1 1.2 y 1.3 Fermi
Multiprocesadores / GPU
Planificación de instrucciones:
Bloques de hilos
Bn
Valor según gener. GPU
Warp 0
…
Warp 2
…
Warp 4
…
Hilo
(31, 0)
Hilo
(32, 0)
Hilo
(31,
Hilo
(32, 1)
Hilo
(31, 2)
Hilo
(32, 2)
Warp 1
…
Warp 3
…
Warp 5
…
Hilo
(63, 0)
Hilo
(63,
Hilo
(63, 2)
DRAM
16
Half Warps
52
Device
Memory
WARPs. Planificación
WARPs. Ejecución
Multiprocesador
! Dado que el tamaño del WARP es de 32 hilos:
! Si hay 8 procesadores en cada multiprocesador (como en las series
8, 9 y 10 de GeForce), cada WARP de una instrucción consume 4
ciclos de reloj en su ejecución.
! Si hay 32 procesadores en cada multiprocesador (a partir de Fermi),
cada WARP de una instrucción consume un solo ciclo.
Warp j (32 hilos)
Bi
Búsqueda de instrucción
L1 datos
Core
Core
SFU
SFU
Warp 5
Instr. 11
Warp 12
Instr. 3
Warp 12
Instr. 4
Warp 11
Instr. 8
Warp 15
Instr. 1
ciclos
! Round-robin/aging para seleccionar el próximo WARP a planificar de
entre aquellos con operandos ya leídos.
! Scoreboarding para evitar riesgos en el análisis de dependencias.
Registros
Memoria compartida
Core
Warp 23
Instr. 17
! El WARP es la unidad de planificación. Se usa:
Instrucción x de warp j
····
······
····
Warp 5
Instr. 10
Core
Core
Core
Core
Core
! El cambio de contexto entre WARPs de un multiprocesador
se lleva a cabo sin penalidad en ciclos de ejecución.
53
Recopilando sobre kernels,
bloques y paralelismo
! Los kernels se lanzan en grids.
! Sólo se ejecuta un kernel a la vez.
! Un bloque se ejecuta en un
multiprocesador.
Escalabilidad transparente
! Dado que los bloques de hilos no pueden sincronizarse,
el hardware tiene libertad para acomodar la ejecución de
un bloque de hilos en cualquier multiprocesador en
cualquier instante.
Grid
Bloque (0, 0)
Bloque (1, 0)
! El bloque no migra.
! Varios bloques pueden residir
concurrentemente en un
multiprocesador.
! Con las consabidas limitaciones:
! 8 bloques concurrentes.
! 512 hilos en cada multiprocesador.
! 768/1024/1536 hilos en total.
Memoria compartida
Memoria compartida
Kernel grid
Device
Regs
Hilo (0, 0)
Regs
Hilo (1, 0)
Regs
Hilo (0, 0)
Regs
Block 0
Hilo (1, 0)
! Y otras limitaciones entran en juego
debido al uso conjunto de la memoria:
! El banco de registros se particiona
entre todos los hilos existentes.
! La memoria compartida se particiona
entre todos los bloques de hilos.
54
Block 2
Memoria global
55
Block 1
Block 3
Block 4
Block 5
Block 6
Block 7
Device
Block 0
Block 1
Block 2
Block 3
Block 4
Block 5
Block 6
Block 7
Block 0
Block 1
Block 2
Block 3
Block 4
Block 5
Block 6
Block 7
! Un kernel puede ejecutarse
sobre cualquier número de multiprocesadores.
! El lanzamiento de un kernel sirve como punto
de sincronización para los bloques.
56
Kernels (y su relación con los hilos)
Hilos (y su relación con los bloques)
! Las porciones paralelas de una aplicación que corre en
la CPU se ejecutan en la GPU como kernels.
! Sólo un kernel se ejecuta en un momento dado en una
GPU (esto es, NO hay multiproceso de kernels).
! Cuando el kernel finaliza, todos los recursos de la GPU
se liberan y quedan disponibles íntegramente para el
kernel siguiente.
! La cooperación entre hilos resulta muy valiosa:
! Según su threadID, cada hilo:
! Ejecuta el mismo código sobre un
área diferente de datos.
! Puede tomar decisiones de control
para diferenciar su ejecución del resto.
0 1 2 3 4 5 6 7
! Comparten resultados para ahorrar computaciones.
! Comparten accesos a memoria de vídeo para reducir drásticamente
el ancho de banda (y el consumo del chip).
! El bloque garantiza rendimiento y escalabilidad, ya que
permite replicar la ejecución de un grupo de hilos tantas
veces como sea necesario en función del volumen de datos:
! Permitiendo mantener el paralelismo de grano fino.
! Sin penalidad, ya que el cambio de contexto es gratis.
threadID
…
float x = input[threadID];
float y = func(x);
output[threadID] = y;
…
Thread Block 0
threadID
0
1
2
3
4
5
Thread Block 0
6
7
…
float x = input[threadID];
float y = func(x);
output[threadID] = y;
…
57
0
1
2
3
4
5
Thread Block N-1
6
7
…
float x = input[threadID];
float y = func(x);
output[threadID] = y;
…
0
…
1
2
3
4
5
6
7
…
float x = input[threadID];
float y = func(x);
output[threadID] = y;
…
Particionamiento de computaciones y datos
Manipulación de datos
! Un bloque de hilos es un lote
de hilos que pueden cooperar:
! Constituye una de las diferencias más importantes entre la
CPU y la GPU, y una de las principales razones para el mayor
rendimiento pico que atesora la GPU.
! El programador gestiona de forma explícita la caché.
! La caché es un millar de veces más pequeña en la GPU, por
lo que el programador debe explotar al máximo la localidad.
! Compartiendo datos a través de
memoria compartida.
! Sincronizando su ejecución para
acceder a memoria sin conflictos.
! Un kernel se ejecuta como una
malla o grid 1D o 2D de
bloques de hebras 1D, 2D o 3D.
! Los hilos y los bloques tienen
IDs para que cada hilo pueda
acotar sobre qué datos trabaja,
y simplificar el dir. a memoria al
procesar datos multidim.
CPU (host)
GPU (device)
Grid 1
Kernel 1
Bloque
(0, 0)
Bloque
(1, 0)
Bloque
(2, 0)
Bloque
(0, 1)
Bloque
(1, 1)
Bloque
(2, 1)
Grid 2
Kernel 2
Memoria Ubicación Caché
Bloque (1, 1)
Hilo
Hilo
(0, 0) (1, 0)
Hilo
(2, 0)
Hilo
Hilo
(3, 0) (4, 0)
Hilo
Hilo
Hilo
Hilo
Hilo
(0, 1) (1, 1) (2, 1) (3, 1) (4, 1)
Hilo
Hilo
Hilo
Hilo
Hilo
(0, 2) (1, 2) (2, 2) (3, 2) (4, 2)
59
Acceso
Ámbito
Lect./escr.
Un hilo
58
Declaración Vigencia
Local
Off-chip
No
__device__
Hilo
Compartida
On-chip
-
Lect./escr. Hilos de un bloque
__shared__
Bloque
Global
Off-chip
No
Lect./escr. Los hilos y la CPU
__global__
Kernel
Constantes
Off-chip
Sí
Lectura
Los hilos y la CPU __constant__
Kernel
De texturas
Off-chip
Sí
Lectura
Los hilos y la CPU
Kernel
__texture__
60
Tipos de memoria y su jurisdicción
Espacios de memoria (1)
Registros
Hilo
Bloque
! La CPU y la GPU tiene espacios de memoria separados:
Integrada
en la GPU
Memoria
compartida
!!!
! 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.
! Los hilos pueden compartir el espacio de memoria
compartida para comunicarse entre ellos.
Grid 0
!!!
!!!
!!!
!!!
!!!
!!!
!!!
!!!
!!!
!!!
!!!
!!!
!!!
!!!
!!!
! Los punteros son sólo direcciones:
Memoria
Memoria
para
global
(lectura y constantes
(lectura)
escritura)
Grid 1
Memoria
para
texturas
(lectura)
! No se puede conocer a través del valor de un puntero si la dirección
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
través de punteros, ya que si un dato de la CPU trata de accederse
desde la GPU o viceversa, el programa se colgará.
Externa
a la GPU
(pero
dentro
de la
tarjeta
gráfica)
! El nivel de memoria global es el único visible a todos los entes
definidos por el programador (hilos, bloques, kernels y grids).
61
Principales debilidades/riesgos de CUDA
Espacios de memoria (2)
Grid en GPU
! Cada hilo puede:
! Leer/escribir registros per-thread
! Leer/escribir mem. compartida per-block
! Leer/escribir mem. global per-grid
! Cada hilo también puede, por motivos de
comodidad o rendimiento:
! Leer/escribir memoria local per-thread
! Leer memoria de constantes per-grid
! Leer memoria de texturas per-grid
! La CPU puede:
! Leer/escribir en memoria
global, constante y de texturas
(mapeadas sobre DRAM)
Bloque (0, 0)
Memoria compartida
Memoria compartida
Registros
Registros
Registros
Registros
Hilo (0, 0)
Hilo (1, 0)
Hilo (0, 0)
Hilo (1, 0)
Memoria
local
Memoria
local
! El ancho de banda entre memoria global (la de la tarjeta
gráfica) y los procesadores puede saturarse fácilmente.
Las tareas que tienen un bajo índice de reutilización de
datos se quedan hambrientas. Ej: Suma vector-escalar,
producto de vectores.
! Limitada capacidad del banco de registros y la memoria
compartida que comparten todos los hilos de un
multiprocesador. Utilizar el CUDA Occupancy Calculator
para ayudarse en la toma de decisiones.
! Los saltos condicionales degradan notablemente el
rendimiento si no se estructuran de forma sabia.
Bloque (1, 0)
Memoria
local
Memoria
local
Memoria global
CPU
(host)
62
Memoria de constantes
Memoria de texturas
63
64
Cinco claves para maximizar
el rendimiento del código
1. Expresar explícitamente todo el paralelismo posible aplicando
SIMD de grano fino para definir multitud de hilos. Recordar
que el cambio de contexto es gratis en CUDA.
1. Si los hilos de un mismo bloque necesitan comunicarse,
utilizar la memoria compartida y __syncthreads()
2. Si los hilos de diferentes bloques necesitan comunicarse,
utilizar la memoria global y descomponer la computación
en múltiples kernels.
2. Aprovechar el ancho de banda con memoria: Pocas
transferencias grandes en lugar de muchas pequeñas.
3. Optimizar la localidad de acceso: Reutilización de datos.
4. Ocultar latencias con memoria global maximizando la
ocupación de unidades funcionales. Intensidad aritmética.
5. Maximizar el CPI del código (throughput): Seleccionar la
instrucción de menor latencia en el repertorio CUDA.
IV. Sintaxis
65
CUDA es C con algunas palabras clave más.
Un ejemplo preliminar
66
La interacción entre la CPU y la GPU
void saxpy_serial(int n, float a, float *x, float *y)
{
for (int i = 0; i < n; ++i)
y[i] = a*x[i] + y[i];
Código C estándar
}
// Invoke serial SAXPY kernel
saxpy_serial(n, 2.0, x, y);
Código CUDA equivalente de ejecución paralela en GPU:
__global__ void saxpy_parallel(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];
}
// Invoke parallel SAXPY kernel with 256 threads/block
int nblocks = (n + 255) / 256;
saxpy_parallel<<<nblocks, 256>>>(n, 2.0, x, y);
67
! CUDA extiende el lenguaje C con un nuevo tipo de función,
kernel, que ejecutan en paralelo los hilos activos en GPU.
! El resto del código es C nativo que se ejecuta sobre la CPU
de forma convencional.
! De esta manera, el típico main() de C combina la ejecución secuencial en CPU y paralela en GPU de kernels CUDA.
! Un kernel se lanza siempre de forma asíncrona, esto es, el
control regresa de forma inmediata a la CPU.
! Cada kernel GPU tiene una barrera implícita a su conclusión,
esto es, no finaliza hasta que no lo hagan todos sus hilos.
! Aprovecharemos al máximo el biprocesador CPU-GPU si les
vamos intercalando código con similar carga computacional.
68
Identificación de los entes al programar
(sobre un ejemplo de suma de matrices)
La interacción entre la CPU y la GPU (cont.)
! BlockIdx: vector (1D o 2D) que identifica el
bloque dentro del grid.
CPU
CPU
! ThreadIdx: vector (1D, 2D o 3D) que identifica
el hilo dentro de su bloque.
GPU
CPU
__global__ void matAdd (float A[N][N],float B[N][N],float 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);
69
{
{
blockIdx = (3,0)
threadIdx = (3,2)
BlockDim.x
BlockIdx.y
es 0
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
BlockIdx.x
es 3
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
Grid de
bloques
}
70
! Modificadores para las funciones ejecutadas en la GPU:
! Cada multiprocesador ejecuta los 32 hilos de un warp
según el paradigma SIMD, esto es, todos los hilos ejecutan
la misma instrucción.
! Si las bifurcaciones afectan de forma diferente a los hilos
de un warp (divergencia), se secuencializa la ejecución de
los hilos del warp (es SIMD, no SPMD), con la consiguiente
pérdida de rendimiento.
! Para evitar esto, existen algunos trucos:
if (ThreadIdx / WARP_SIZE >2)
¿Qué hilo computa C[15,2]?
Modificadores para las funciones y
lanzamiento de ejecuciones en GPU
Control de flujo para el programa
if (ThreadIdx.x > 2)
Las tres matrices se
particionan igual,
otorgando un
elemento a cada hilo
! Para cada uno de los 4x4 hilos del bloque:
GPU
Un kernel no comienza su ejecución en GPU hasta que
no hayan finalizado todas las llamadas CUDA anteriores.
}
Matrices A, B y C
de 16x16 elementos
! Para cada uno de los 4x4 bloques del grid:
blockDim.y
Ejecución
__global__ kernelA(){···}
__global__ kernelB(){···}
int main()
···
kernelA <<< dimGridA, dimBlockA >>> (params.);
···
kernelB <<< dimGridB, dimBlockB >>> (params.);
···
! __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]; // En mem. caché
! __constant__ float MyConstantArray[32];
! Configuración de la ejecución 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.
}
Con divergencia
Sin divergencia
71
72
Para conocer en tiempo de ejecución
los recursos hardware con los que contamos
Variables intrínsecas y funciones accesibles
! dim3 gridDim;
! dim3 blockDim;
! Cada GPU disponible en la capa hardware recibe un número
entero consecutivo que la identifica, comenzando por el 0.
! Para conocer el número de GPUs disponibles:
// Dimensión del grid
// Dimensión del bloque
! cudaGetDeviceCount(int* count);
! Para conocer los recursos disponibles en la GPU dev
(caché, registros, frecuencia de reloj, ...):
! uint3 blockIdx; // Indice del bloque dentro de la malla
! uint3 threadIdx; // Indice del hilo dentro del bloque
! cudaGetDeviceProperties(struct cudaDeviceProp* prop, int dev);
! Para conocer la mejor GPU que reúne ciertos requisitos:
! void __syncthreads(); // Sincronización entre threads
! cudaChooseDevice(int* dev, const struct cudaDeviceProp* prop);
! Para seleccionar una GPU concreta:
El programador debe elegir el tamaño del bloque
y el número de bloques para explotar al máximo
el paralelismo del código durante su ejecución.
! cudaSetDevice(int dev);
! Para conocer en qué GPU estamos ejecutando el código:
73
Sincronización entre la CPU y la GPU
cuando disponemos de varias GPUs
! cudaGetDevice(int* dev);
74
Para gestionar la memoria de vídeo
! Cada hilo de la CPU sólo puede controlar una GPU, por lo
tanto, necesitamos declarar tantos hilos en CPU (usando
pthreads) como GPUs queramos poner a trabajar.
! Una CPU no puede controlar varias GPUs, pero una GPU sí
puede ser accedida desde varios hilos de la CPU. El driver
de la GPU se encarga de serializar las llamadas.
! Los recursos CUDA alojados desde un hilo de la CPU sólo
pueden ser utilizados por llamadas CUDA procedentes de
ese mismo hilo. Ejemplo de mal uso:
! Para reservar y liberar memoria en la GPU:
y cudaFree(p)
! Para mover áreas de memoria entre CPU y GPU,
tras declarar malloc(h_A) en la CPU y
cudaMalloc(d_A) en la GPU:
! cudaMalloc(void* p, size_t numBytes)
! Desde la CPU a la GPU:
! cudaMemcpy(d_A, h_A, numBytes, cudaMemcpyHostToDevice);
! Desde la GPU a la CPU:
! cudaMemcpy(h_A, d_A, numBytes, cudaMemcpyDeviceToHost);
! El hilo 2 de la CPU aloja memoria en GPU y almacena su dirección
en el puntero P.
! El hilo 3 realiza una llamada a CUDA pasando P como parámetro.
75
76
Ejemplo 1: Descripción
Ejemplo 1: Implementación
int main()
{
int dimx = 16;
int num_bytes = dimx*sizeof(int);
int *d_a=0, *h_a=0; // device and host pointers
! 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.
h_a = (int*)malloc(num_bytes);
cudaMalloc( (void**)&d_a, num_bytes );
if( 0==h_a || 0==d_a ) printf("couldn't allocate memory\n");
cudaMemset( d_a, 0, num_bytes );
cudaMemcpy( h_a, d_a, num_bytes, cudaMemcpyDeviceToHost );
for(int i=0; i<dimx; i++) printf("%d ", h_a[i] );
free( h_a );
cudaFree( d_a );
77
}
78
Ejemplo 2: Incrementar un valor “b”
a los N elementos de un vector
Transferencias de memoria asíncronas
! Las llamadas a cudaMemcpy() son síncronas, esto es:
Programa C en CPU
! 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.
void increment_cpu(float *a, float b, int N)
{
for (int idx = 0; idx<N; idx++)
a[idx] = a[idx] + b;
! A partir de CUDA Compute Capabilities 1.2 es posible
utilizar la variante cudaMemcpyAsync(), cuyas
diferencias son las siguientes:
}
void main()
{
.....
increment_cpu(a, b, N);
}
! El retorno a la CPU tiene lugar de forma inmediata.
! Podemos solapar comunicación y computación.
! En la sección “Estrategias de mejora” pondremos un ejemplo.
79
Programa CUDA en GPU
__global__ void increment_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) );
increment_gpu<<<dimGrid, dimBlock>>>(a, b, N);
}
62
80
Código en CPU para el ejemplo 2
(azul es C, verde es CUDA, rojo son vars.)
Ejemplo 2: Incrementar un valor “b”
a los N elementos de un vector
Extensiones
al lenguaje
Con N=16 y blockDim=4, tenemos 4 bloques de hilos,
encargándose 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
int idx = (blockId.x * blockDim.x) + threadIdx.x;
Se mapeará del índice local threadIdx al índice global
// 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);
blockIdx.x=3
blockDim.x=4
threadIdx.x=0,1,2,3
idx=12,13,14,15
// ejecuta el kernel. B es es número de bloques
increment_gpu <<< N/blockSize, blockSize >>> (d_A, b);
// copia los datos de regreso a la CPU
cudaMemcpy(h_A, d_A, numBytes, cudaMemcpyDeviceToHost);
Patrón de acceso
común
Nota: blockDim debería ser >= 32 (warp size) en código real, esto es sólo un ejemplo
63
// aloja memoria en la CPU
unsigned int numBytes = N * sizeof(float);
float* h_A = (float*) malloc(numBytes);
81
// libera la memoria de vídeo
cudaFree(d_A);
64
Interoperabilidad con los programadores
de los API gráficos (OpenGL y Direct3D)
! Manejo de texturas:
V. Compilación
! cudaBindTexture()
! cudaBindTextureToArray()
! Interoperabilidad con los API gráficos:
! Ejemplo con OpenGL: cudaGLMapBufferObject()
! Ejemplo con Direct3D: cudaD3D9MapVertexBuffer()
! Vertex Buffer Objects y Pixel Buffer Objects, tanto de
OpenGL como de Direct3D, pueden mapearse sobre el
espacio de direcciones de CUDA, y luego se manejan
como memoria global en el código de la GPU.
! Los datos de una imagen alojados en Pixel Buffer Objects
pueden mostrarse con glDrawPixels/glTexImage2D.
83
84
82
El proceso de compilación
void serial_function(… )
{
...
}
void other_function(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(..);
...
}
Kernels
CUDA
NVCC
(Open64)
Los diferentes módulos de compilación
! El código fuente CUDA se
compila con NVCC.
Resto del
código C
! NVCC separa el código que
se ejecuta en CPU del que lo
hace en GPU.
Compilador
de la CPU
! La compilación se realiza
en dos etapas:
Identificar
los kernels
Ficheros objeto
CUDA y rees- Ficheros objeto
de la CPU
CUDA
Enlazador
cribirlos para
aprovechar
paralelismo
en GPU
Ejecutable
! Virtual: Genera código PTX
(Parallel Thread eXecution).
! Física: Genera el binario para
una GPU específica (o incluso
para una CPU multicore - ver
un poco más adelante).
CPU-GPU
C/C++ CUDA
Application
Código
fuente
NVCC
Código CPU
Virtual
PTX Code
Físico
PTX to Target
Compiler
G80
…
GPU
85
Código
objeto
86
Compilador NVCC y máquina virtual PTX
NVCC (Nvidia CUDA Compiler)
C/C++ CUDA
Application
! NVCC es un driver del compilador.
float4 me = gx[gtid];
me.x += me.y * me.z;
! EDG
! Funciona invocando todos los compiladores y herramientas
necesarias como cudacc, g++, cl, ...
! Separa código GPU y CPU.
! NVCC produce como salida:
EDG
! O bien código C para la CPU, que debe luego compilarse con el
resto de la aplicación utilizando otra herramienta.
! O bien directamente código objeto PTX.
CPU Code
! Open64
! Genera ensamblador PTX.
! Parallel Thread eXecution
(PTX)
! El ejecutable CUDA usa dos librerías dinámicas:
! Máquina virtual e ISA.
! Modelo de programación.
! Recursos y estado de ejecución.
Open64
! The CUDA runtime library (cudart)
! The CUDA core library (cuda)
PTX Code
ld.global.v4.f32
mad.f32
87
{$f1,$f3,$f5,$f7}, [$r9+0];
$f1, $f5, $f3, $f1;
88
Depuración: Modo de emulación del
dispositivo (Device Emulation Mode)
! Un ejecutable compilado en modo de emulación del
dispositivo (nvcc -deviceemu) corre íntegramente en la
CPU (host) usando el soporte en tiempo de ejecución de
CUDA sin necesidad de GPU ni driver.
! Posibilidades de modo de emulación:
VI. Depuración y optimización
! Utilizar el soporte para la depuración de que disponga la CPU
(breakpoints, inspección de registros, etcétera).
! Acceder a los datos de la GPU desde el código de la CPU.
! Llamar a cualquier función de la CPU desde el código de la GPU
(por ejemplo, printf) y viceversa.
! Detectar situaciones de interbloqueo por el uso indebido de
__syncthreads.
90
89
Carencias del modo de
emulación del dispositivo
CUDA Visual Profiler
! Los hilos del código se ejecutan de forma secuencial, por lo
que el acceso simultáneo a la misma posición de memoria
desde múltiples hilos produce (potencialmente) resultados
distintos.
! El acceso a valores mediante punteros de GPU en la CPU o
punteros de CPU en la GPU puede producir resultados
correctos en el modo de emulación, pero dará un error al ser
ejecutado.
! Los resultados de la computación en punto flotante diferirán
ligeramente debido a:
! Proporciona ciertas mediciones estratégicas para la
localización de problemas de rendimiento.
! Temporización entre GPU y CPU para las invocaciones a los kernels
y las llamadas a memcpy.
! Evolución a determinados pasos de tiempo.
! Acceso a contadores de rendimiento hardware.
! Las diferentes salidas del compilador.
! Diferentes conjuntos de instrucciones.
! El uso de precisión extendida para la generación de resultados
intermedios.
91
92
Contadores que proporciona el profiler
Interpretación de los contadores del profiler
! Representan los eventos dentro de un warp de hilos.
! Involucran a un solo multiprocesador
! Se sigue la pista de ciertos eventos definidos por el programa a través de contadores
hardware que registran determinadas señales emitidas por el chip:
!
!
!
!
gld_incoherent
gld_coherent
gst_incoherent
gst_coherent
! Los valores no se corresponderán con el número total de warps
lanzados para un kernel dado.
! Debemos lanzar un número suficiente de bloques de hilos para
asegurar que el multiprocesador involucrado tiene asignado un
porcentaje consistente del trabajo total.
Cargas/almac. en memoria global son coherentes
(“coalesced”) o incoherentes (“non-coalesced”)
! local_load
! local_store
Cargas y almacenamientos locales
! branch
! divergent_branch
Número total de bifurcaciones (“branches”) y
bifurcaciones divergentes tomadas por los hilos
! instructions – cuenta del número de instrucciones ejecutadas.
! warp_serialize – warps de hilos que se han sido secuencializados por los conflictos
de direccionamiento a la memoria compartida o a la memoria de constantes.
! cta_launched – bloques de hilos que han sido ejecutados.
! Los valores se utilizan principalmente para cuantificar la
mejora de rendimiento producida por una versión
optimizada del código.
! Se trata de reducir gld/gst_incoherent, divergent_branch
y warp_serialize.
93
94
Maximizar la ocupación
para ocultar la latencia
Restricciones en la configuración de la
ejecución
! Dónde se producen las latencias:
! Máximo nº de hilos por bloque: 512
! Nº de hilos activos limitados por los recursos.
! En el acceso a memoria global: 400-600 ciclos.
! En dependencias read-after-write entre registros.
! Nº de registros por multiprocesador: 8192 ó 16384 (serie 10 en
adelante).
! Cantidad de memoria compartida por multiprocesador: 16K ó 48K (Fermi
en adelante).
! El resultado de las instrucciones sólo puede leerse 11 ciclos más tarde.
! Se bloquean las instrucciones que presentan
dependencias en el mismo hilo, pero no las que se
encuentran en otros hilos. Para ocultar esta latencia,
debemos ejecutar tantos hilos por multiprocesador como
sea posible, eligiendo los parámetros de ejecución que
maximicen:
! Utilizar el flag–maxrregcount=N en NVCC
! N = nº máximo de registros deseado por kernel.
! En algún momento puede sobrevenir un “spilling” a LMEM (trasvase
entre el banco de registros y la memoria local que emplea el compilador
para su uso interno), con el consiguiente impacto en el rendimiento.
ocupación = (nº warps activos)/(máx. nº warps activos)
! Rastrear el archivo .cubin para descubrir el uso de LMEM
(ver la siguiente diapositiva).
Nota: El máximo nº de warps activos en las series 8, 9 y 10 de GeForce es de 24, y en
Fermi es de 48.
95
96
Heurísticos para la configuración
de la ejecución
Para conocer la utilización de los recursos
! Compilar el código del kernel con el flag -cubin para
conocer cómo se están usando los registros.
! Abrir el archivo .cubin con un editor de texto y mirar la
sección “code”.
! El número de hilos por bloque debe ser un múltiplo de 32.
! Para no desperdiciar en la ejecución de warps incompletos
! El número de bloques debe superar al de multiprocs.
! Para que todos ellos tengan al menos un bloque que ejecutar.
! Los recursos por bloque (registros y memoria compartida)
deben ser al menos la mitad del total disponible.
architecture {sm_10}
Memoria local para cada hilo
abiversion {0}
(usada por el compilador para
modname {cubin}
volcar contenidos de los registros
code {
en memoria)
name = BlackScholesGPU
lmem = 0
Memoria compartida usada
smem = 68
por cada bloque de hilos
reg = 20
bar = 0
Registros usados
bincode {
0xa0004205 0x04200780 0x40024c09 0x00200780 por cada hilo
…
! De lo contrario, resulta mejor fusionar bloques.
! El número de bloques debe superar en más del doble al
de multiprocesadores.
! Para tener siempre más de un bloque activo por multiprocesador que
garantice su ocupación en caso de que alguno se bloquee.
! El número de bloques debe superar el centenar.
! Para que el código sea escalable en futuras generaciones.
97
Heurísticos para la configuración
de la ejecución (cont.)
98
Parametrización de una aplicación
! Conflicto: Más hilos por bloque significa mejor ocultación
de latencia, pero menos registros por hilo.
! Sugerencia: Utilizar un mínimo de 64 hilos por bloque, o
incluso mejor, 192 ó 256 hilos (si aún se dispone de
registros suficientes para ello).
! Conflicto: Incrementar la ocupación no significa
necesariamente aumentar el rendimiento, pero una baja
ocupación del multiprocesador no permite ocultar latencias
en kernels limitados por el ancho de banda a memoria.
! Sugerencia: Prestar atención a la intensidad aritmética y al
paralelismo disponible.
! Todo lo que concierne al rendimiento es dependiente de la
aplicación, por lo que hay que experimentar con ella para
lograr resultados óptimos.
! Las GPUs evolucionan en muchos aspectos:
! El número de multiprocesadores. 16 en la serie 8, 30 en la serie 10,
16 de nuevo en Fermi (pero con 32 cores cada uno).
! Ancho de banda con memoria: En torno a 100 GB/s.
! Tamaño de la memoria compartida: 16 KB. para cada
multiprocesador, salvo en Fermi, que puede ampliarse a 48 KB.
! Tamaño del banco de registros: 8192 en la serie 8, 16384 en la
serie 10, 4096 en Fermi.
! Hilos: Ahora, hasta 512 por bloque. 768, 1024 ó 1536 en total
según tengamos una serie 8, una serie 10 o una arquitectura Fermi.
99
100
CUDA Occupancy Calculator
! Asesora en la selección de los parámetros de configuración
VII. Estrategias de mejora
101
102
(1) Solapamiento de
computación y comunicaciones
(1) Solapamiento de
computación y comunicaciones (cont.)
! Posibilidades:
! Ejemplo 1 (notar que 0 es el stream por defecto):
! Solapar computación en CPU con transferencias de datos a la GPU.
Es posible en cualquier dispositivo CUDA, aprovechando que el
lanzamiento de kernels desde la CPU es asíncrono. Sencillo de
implementar.
! Solapar la computación de un kernel en GPU con transferencias de
datos a la GPU. Sólo es posible a partir de CUDA Compute Cap. 1.1.
Más complejo de implementar, pues necesitamos tres cosas:
cudaMemcpyAsync(A_d, A_h, size, cudaMemcpyHostToDevice, 0);
kernel<<<grid, block>>>(A_d);
cpuFunction();
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
cudaMemcpyAsync(dst, src, size, dir, stream1);
kernel<<<grid, block, 0, stream2>>>(…);
cudaMemcpyAsync(dst2, src2, size, stream2);
cudaStreamQuery(stream2);
103
solape
! Ejemplo 2 (las dos últimas sentencias no suponen solape adicional):
1. Alojar la memoria pinned en CPU (para que no pagine) usando
cudaMallocHost().
2. Definir streams. Un stream es una secuencia de operaciones CUDA que se
ejecutan de forma ordenada.
3. Transferir los datos CPU ! GPU usando las variantes asíncronas de copia de
datos (cudaMemcpyAsync(dst,src,size,dir,stream)), que devuelven
inmediatamente el control a la CPU.
solape
! http://developer.download.nvidia.com/compute/cuda/CUDA_Occupancy_calculator.xls
104
(3) Optimizar los patrones
de acceso a memoria
(2) Optimizar el uso de la memoria
! Minimizar las transferencias entre CPU y GPU.
! El ancho de banda efectivo puede variar un orden de
magnitud dependiendo del patrón de acceso si sabemos
utilizar las siguientes armas:
! Recordar que este ancho de banda es muy inferior al ancho de
banda de la memoria de vídeo.
! En los casos en que se quiera aumentar el ancho de banda, utilizar
memoria “pinned” (sin abusar), que aprovecha casi todas las
prestaciones de PCI-express (3.2 GB/s. en v1.0 y 5.2 GB/s. en v2.0
normalmente, 4 y 8 GB/s. si usamos el chipset nForce 680i).
! Accesos coalesced a memoria global (menos importante en Fermi).
! Accesos a memoria compartida sin conflictos a sus 16 bancos.
! Accesos a memoria de texturas (que pasa por caché).
! Accesos a memoria de constantes que tienen una misma dirección.
! Agrupar las transferencias de datos entre CPU y GPU
! Recordar que:
! Ya que la latencia predomina en el coste sobre el ancho de banda.
! Procesar datos es más rápido que moverlos, ya que las GPUs
dedican muchos más transistores a las ALUs que a la memoria.
! Cuanto menos estrangulado se encuentre un kernel por el acceso a
memoria, mejor se comportará en las arquitecturas GPU futuras.
! Pasar algunas funciones de CPU a GPU aunque no puedan
explotar mucho paralelismo
! Si eso evita un doble trasiego de datos de GPU a CPU y regreso.
105
(5) Eliminar los conflictos en el acceso a
los 16 bancos de memoria compartida
(4) Uso de la memoria compartida
Si el tamaño se conoce
en tiempo de compilación
__global__ void kernel (...)
{
...
__shared__ float sData[256];
...
}
void main()
{
...
kernel<<<nBlocks,blocksize>>>(...);
...
}
106
! Se dispone de un espacio total de
16 Kbytes en cada multiprocesador.
! Se estructura en 16 bancos
con palabras de 32 bits
y entrelazado de orden inferior.
! El acceso simultáneo de los hilos a
memoria en cada medio warp se
produce en cualquiera de las dos
situaciones siguientes:
1. Siempre que los hilos accedan
a bancos diferentes (cualquier
permutación).
2. Si todos los hilos acceden a la
misma palabra de un banco.
Si el tamaño se desconoce
en tiempo de compilación
__global__ void kernel (...)
{
...
extern __shared__ float sData[];
...
}
void main()
{
...
smBytes = blockSize * sizeof(float);
kernel<<<nBlocks,blocksize,smBytes>>>(...);
...
}
107
Caso 1:
Caso 2:
108
(5) Eliminar los conflictos en el acceso a
los 16 bancos de memoria compartida (cont.)
! Esta optimización puede resultar bastante desafiante, aunque existe un
truco para llevarla a cabo de forma general ajustando los índices de acceso
al ID de cada grupo de trabajo:
VIII. Ejemplos
__shared float mem[N];
size_t localIdx = get_local_id[1];
for (int=0; i<N; i++)
float val = mem[i+localIdx % N];
! El precio a pagar respecto a una optimización manual es la pérdida por
computar la aritmética módulo, aunque si N es potencia de 2, se puede
reemplazar por un operador AND bit-wise mucho más eficiente:
assert (N is a power of 2);
__shared float mem[N];
size_t localIdx = get_local_id[1];
for (int=0; i<N; i++)
float val = mem[i+localIdx & (N-1)];
109
110
Pasos a realizar para
la construcción del código
1. Identificar las partes potencialmente paralelas.
2. Delimitar los datos necesarios.
3. Mover los datos a la GPU.
4. Llamar al kernel de computación.
5. Establecer la sincronización apropiada entre GPU y CPU.
6. Transferir los resultados de la GPU a la CPU.
7. Integrar los resultados de la GPU en variables de la CPU.
VIII.1. Suma de dos vectores
111
112
Código necesario para
el kernel y su invocación
// Suma de los vectores C = A+B
// Cada hilo calcula un componente del vector
__global__ void vecAdd(float* A, float* B, float* C)
{
!
Int tid = threadIdx.x+ blockDim.x* blockIdx.x;
!
C[tid] = A[tid] + B[tid];
}
Código en CPU para el manejo de memoria
y la recolección de resultados desde la GPU
Código GPU
int main()
{ // Lanzamos N/256 bloques de 256 hilos cada uno
!
vecAdd<<< N/256, 256>>>(d_A, d_B, d_C);
Código CPU
}
113
unsigned int numBytes = N * sizeof(float);
// aloja memoria en la CPU
float* h_A = (float*) malloc(numBytes);
float* h_B = (float*) malloc(numBytes);
... inicializa h_A y h_B ...
// aloja memoria en la GPU
float* d_A = 0; cudaMalloc((void**)&d_A, numbytes);
float* d_B = 0; cudaMalloc((void**)&d_B, numbytes);
float* d_C = 0; cudaMalloc((void**)&d_C, numbytes);
// copia los datos de entrada de la CPU en la GPU
cudaMemcpy(d_A, h_A, numBytes, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, numBytes, cudaMemcpyHostToDevice);
... LA LLAMADA AL KERNEL vecAdd ANTERIOR SE REALIZA AQUI ...
// copia los resultados de la GPU en la CPU
float* h_C = (float*) malloc(numBytes);
cudaMemcpy(h_C, d_C, numBytes, cudaMemcpyHostToDevice);
// libera la memoria de vídeo
cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
64
114
Resumen de actuaciones a realizar para
mejorar el rendimiento del código CUDA
CUDA permite lograr excelentes resultados en la
paralelización de código si respetamos unas pocas
premisas de rendimiento:
VIII. Producto de matrices
1. Estructurar el código y elegir una configuración de hilos que
maximice el paralelismo de datos en GPU y minimice la
transferencia de datos CPU ! GPU.
2. Respetar la coherencia al acceder a memoria global (coalescing).
3. Maximizar el uso de la memoria compartida (tiling).
4. Evitar accesos a memoria compartida con elevado número de
conflictos al acceder a sus bancos.
5. Minimizar el uso de warps divergentes.
Vamos a aplicar 1, 2 y 3 sobre el producto de matrices.
115
116
Optimización 1: Estructurar el código
para maximizar el paralelismo
Optimización 2: Coalescing para
maximizar el acceso a memoria global
! Declarar muchos threads permite ocultar latencias con memoria global.
! Máxima granularidad del código: Utilizar un único grid con tantos hilos
como elementos haya en la matriz C, de forma que cada hilo computa un
único elemento de la matriz resultado C.
! Entre el grid y los hilos se sitúa el bloque para acomodar los hilos en
grupos de a lo sumo 512 threads (máx. paralelismo en CUDA). Y como 512
no es un cuadrado perfecto, 256 resulta el candidato ideal (256 = 16x16)
para equilibrar el paralelismo en las dos dimensiones de la matriz.
Grid
WidthA
WidthB
Bloque
····
······ !!!!!!!
····
····
······
····
!!!!!!!!!!!!!!!
····
!!!!!!!
· · · · · · Th(x,y)
····
····
······
····
! Las instrs. pueden leer/escribir palabras de 32, 64 o 128 bits.
! Medio warp puede acceder de forma unificada (coalesced) para
leer 32, 64 o 128 bytes a segmentos de memoria global.
! Segmento: Intervalo continuo de memoria de 32 , 64 o 128 bytes,
alineado a estos tamaños.
K mod 32
WidthB
32 bytes
K mod 128
K mod 64
64 bytes
HeightA
C(x, y)
C
=
HeightA
X
A
128 bytes
B
dim2 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
dim2 dimGrid(WidthB/BLOCKSIZE, HeightA/BLOCKSIZE);
Mem. global
Mem. global
Mem. global
117
Coalescing (3)
Coalescing (2)
! Condiciones para el coalescing de hilos en CUDA 1.0 y 1.1
! Condiciones de coalescing para CUDA 1.2 o
superior:
! Acceso a palabras de 32 bits (un acceso de 64 bytes), de 64 bits (dos accesos de
128 bytes) o de 128 bits (cuatro accesos de 128 bytes)
! Las 16 palabras deben estar ubicadas en un segmento de memoria de igual tamaño
al de la transacción (o del doble en el caso de 128 bits)
! Los hilos deben acceder en secuencia (el k-ésimo hilo a la k-ésima palabra)
! Los hilos acceden a datos ubicados en un segmento
de memoria de:
! 32 bytes (datos de 8 bits).
! 64 bytes (datos de 16 bits).
! 128 bytes (datos de 64 bits).
! Si las condiciones no se cumplen, cada hilo requerirá un nuevo acceso.
Coalesced
118
No-coalesced
! El patrón de acceso puede ser cualquiera, e incluso
varios hilos pueden acceder al mismo dato.
! El número simultáneo de accesos a memoria, n,
será aquel que pueda acomodarse en medio warp
conformando n segmentos.
119
120
Optimización 2: Ejemplo de coalescing sobre
el producto de matrices
Optimización 3: Tiling aplicado
al producto de matrices
! C = A ! B. Matrices de N x N datos.
! Sin tiling:
Coalescing en acceso a la matriz B:
B
! Requiere un gran
ancho de banda.
! Tiene poca
intensidad
aritmética.
Memoria global
! Cada hilo necesita 10 regs., por lo que podemos declarar
768 hilos estructurados en 3 bloques de 256 (16x16).
Rendimiento: 10.58 GFLOPS
Mirando el PTX del código, el cuello de botella se sitúa en
el acceso a memoria global.
A
N
! Cada hilo computa un elemento de C.
! A y B se cargan N veces de memoria de vídeo.
C
N
A
Ctemp=0;
for (i=0; i<widthA; i++){
Ctemp += A[indexA] * B[indexB];
indexA++;
indexB += widthB;
}
C[indexC] = Ctemp ;
B
N
N
121
Optimización 3: Tiling aplicado
al producto de matrices (2)
Optimización 3: Implementación del tiling
B
Tenemos que particionar los datos en subconjuntos (tiles) que
quepan en los 16 Kbytes de memoria compartida…
… y manipular cada subconjunto desde un bloque de hilos:
M
! Cada bloque de hilos se encarga de una
submatriz Csub de MxM elementos de C.
N
M
! Con tiling:
! A y B se cargan sólo (N/M) veces desde
memoria de vídeo.
! Cargar el subconjunto desde memoria global a memoria compartida.
! __syncthreads()
! Realizar la computación del subconjunto en memoria compartida.
M
A
C
! Cada hilo puede iterar eficientemente sobre cualquier elemento de datos.
! __syncthreads() (en caso de necesidad por dependencias)
! Copiar los resultados desde memoria compartida a memoria global.
M
M
N
M
N
Csub
M
! Relaja el ancho
de banda.
! Aumenta la
intensidad
aritmética.
122
M
N
123
124
Optimizaciones adicionales:
Loop unrolling
Optimización 3: Implementación del tiling
GRID
!!!!!!!!!!
SM0
As
!!!!
!!!!
!!!!
Bs
!!!!
!!!!
!!!!
8192 registros
SP0
SM15
Shared Memory
8 KB constant
!!!!!!!!!!!!!!
SFU0
SP7
SFU1
! Cada hilo carga un elemento
del tile de A y B
¡El compilador también puede hacer cosas por nosotros!
Ctemp=0;
for (!!!){
__shared__ float As[16][16];
__shared__ float Bs[16][16];
// Load tile (16x16)
As[ty][tx] = A[indexA];
Bs[ty][tx] = B[indexB];
indexA += 16;
indexB += 16 * widthB;
__syncthreads();
// Compute results from tile
for (i=0; i<16; i++)
Ctemp+=As[ty][i]*Bs[i][tx];
__syncthreads();
}
C[indexC] = Ctemp ;
Ctemp=0;
for (!!!){
__shared__ float As[16][16];
__shared__ float Bs[16][16];
// Load tile (16x16)
As[ty][tx] = A[indexA];
Bs[ty][tx] = B[indexB];
indexA += 16;
indexB += 16 * widthB;
__syncthreads();
// Compute results from tile
for (i=0; i<16; i++)
Ctemp+=As[ty][i]*Bs[i][tx];
__syncthreads();
}
C[indexC] = Ctemp ;
Ctemp=0;
for (!!!){
__shared__ float As[16][16];
__shared__ float Bs[16][16];
// Load tile (16x16)
As[ty][tx] = A[indexA];
Bs[ty][tx] = B[indexB];
indexA += 16;
indexB += 16 * widthB;
__syncthreads();
// Compute results from tile
Ctemp+=As[ty][0]*Bs[0][tx];
!!!!
Ctemp+=As[ty][15]*Bs[15][tx];
__syncthreads();
}
C[indexC] = Ctemp ;
!!!!
!!!!
!!!!
!!!!
!!!!
!!!!
A
B
Memoria global
125
126
Rendimiento de tiling & unrolling
VIII. 3. Incremento de un escalar
a un vector de float3
100
75
GFLOPS
50
Sólo tiling
Tiling & Unrolling
25
0
4x4
8x8
12x12
16x16
Tamaño del tile
127
128
Dónde está el problema: Coalescing
Código de partida
! float3 ocupa 12 bytes.
! Cada hilo concluye ejecutando tres lecturas.
__global__ void accessFloat3(float3 *d_in, float3 d_out)
{
int index = blockIdx.x * blockDim.x + threadIdx.x;
float3 a = d_in[index];
! sizeof(float3) ! 4, 8, o 16.
! Cada half-warp lee tres regiones no-contiguas de 64 bytes.
a.x += 2;
a.y += 2;
a.z += 2;
d_out[index] = a;
t0
t1
t2
float3
float3
float3
t3
! Debemos cambiar el patrón de acceso para que los hilos
accedan a memoria global de forma consecutiva.
}
129
Ideas para llevar a cabo
la implementación
Cómo resolver el problema
MEMORIA GLOBAL
…
Paso 2
Paso 1
…
t0 t1 t2
t255
…
…
MEMORIA COMPARTIDA
130
t0 t1 t2
…
…
…
MEMORIA COMPARTIDA
! Usar la memoria compartida para permitir coalescing
! Se necesitan sizeof(float3)*(hilos/bloque) bytes de memoria compartida.
! Cada hilo lee tres escalares de tipo float:
! Desplazamiento: 0, (hilos/bloque), 2*(hilos/bloque).
! Estos datos serán presumiblemente procesados por otros hilos, por lo que se
requiere sincronizar.
! Procesamiento
! Cada hilo recupera su float3 del vector en memoria compartida
! Para ello, hay que convertir el puntero a memoria en (float3*) y utilizar threadID
como índice.
! El resto del código no requiere modificación alguna.
De forma similar, el paso 3 comienza
desplazado 512 posiciones.
131
132
Codificación en CUDA
Leer operandos
fuente a través
de memoria
compartida
Rendimiento obtenido
__global__ void accessInt3Shared(float *g_in, float *g_out)
{
int index = blockIdx.x * blockDim.x + threadIdx.x;
__shared__ float s_data[256*3];
s_data[threadIdx.x]
= g_in[index];
s_data[threadIdx.x+256] = g_in[index+256];
s_data[threadIdx.x+512] = g_in[index+512];
__syncthreads();
float3 a = ((float3*)s_data)[threadIdx.x];
Este código
permanece
inalterado
a.x += 2;
a.y += 2;
a.z += 2;
Escribir el
resultado a
través de
memoria
compartida
((float3*)s_data)[threadIdx.x] = a;
__syncthreads();
g_out[index]
= s_data[threadIdx.x];
g_out[index+256] = s_data[threadIdx.x+256];
g_out[index+512] = s_data[threadIdx.x+512];
}
! Condiciones experimentales:
! Creamos un kernel que lee un array de floats, incrementa sus
posiciones y escribe el resultado.
! Utilizamos 3 millones de floats (12 Mbytes)
! Promediamos los tiempos sobre 10000 ejecuciones.
! Para float: 12K bloques de 256 hilos.
! 356"s – coalesced.
! 3,494"s – acceso permutado/desalineado por cada hilo.
! Para float3: 4K bloques de 256 hilos:
! 3,302"s – NO coalesced.
! 359"s – Coalesced a través de memoria compartida.
133
134
Bibliografía
Herramientas software
! GPU Gems, Gems 2 y Gems 3.
! CUDA Programming Guide. Las bases de CUDA.
! CUDA Best Practices Guide. Para optimizar código.
! CUDA Zone (http://www.nvidia.com/cuda).
! CUDA Toolkit, ya en su versión 3.0 para Fermi.
! Compilador nvcc, librerías y documentación.
! Implementaciones disponibles para Linux, MacOS y Windows 7.
! CUDA SDK:
! Scripts de compilación y algunas utilidades.
! Códigos de ejemplo y whitepapers.
! Los códigos que se han desarrollado en CUDA junto a los factores
de aceleración logrados.
! Los artículos de investigación que describen las aplicaciones y su
implementación.
! Tutoriales, forums, curso de programación paralela de la UIUC, ...
! GPGPU: El movimiento originario de la programación de
propósito general sobre la GPU que desde 2003 aglutina
las novedades más interesantes. http:///www.gpgpu.org
! CUDA Occupancy Calculator.
! CUDA Profiler: Analiza tiempos de ejecución, accesos
coalesced a memoria, warp divergentes, conflictos en
el acceso a memoria compartida, ...
! Nvidia Nexus: Entorno de desarrollo similar a Visual Studio.
135
136