Está en la página 1de 115

1

CUDA
1. Introducción
2. Arquitectura
3. Programación
4. Compilación, depuración y monitorización
5. Estrategias de mejora
6. Bibliografía
2

CUDA
1. Introducción
2. Arquitectura
3. Programación
4. Compilación, depuración y monitorización
5. Estrategias de mejora
6. Bibliografía
3

¿Qué es CUDA?
“Compute Unified Device Architecture”
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 un lenguaje de alto nivel


con mínimas pero potentes extensiones para lograr una ejecución eficiente
y escalable.

• A nivel firmware, ofrece un driver para la programación que es


compatible con el que se usa para renderizar.

• A nivel hardware, habilita múltiples niveles de paralelismo. Cambios


según generaciones:
! 16-30-16-16-24-60-… multiprocesadores sobre los que se lanzan
bloques de threads.
! Multiprocesadores de 8-8-32-192-128-64-… cores (SPs) sobre los que
se ejecutan threads.
! Jerarquía de memoria: Registros, memoria compartida, cachés y global
(DRAM).
4

¿Qué es CUDA?
• Lenguaje de alto nivel (C, C++, Fortran, …) con mínimas extensiones:
! El programador escribe el programa para un solo thread, y el código se
instancia de forma automática 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 programación:

– Basado en el paralelismo masivo de datos y usando gran cantidad de


threads (SIMT).
– Escalable: El código se ejecuta sobre cualquier número de cores sin
recompilar.
! Un modelo de gestión de la memoria: Más explícita al programador.

• Objetivos:
! Construir código escalable a cientos de cores de forma sencilla,
permitiendo declarar miles de threads.
! Permitir computación heterogénea en CPU y GPU.
5

CUDA: Relación con la CPU

Host Thread Grid of Thread Blocks

...

GPU

SMem

SMem

SMem
CPU

Cache Cache

Host
Device Memory
Memory
6

Objetivos de CUDA
• Habilitar paralelismo masivo en GPU 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:
! Básico: Posibilita una fácil portabilidad desde C, C++, …
! Medio: Requiere un buen conocimiento de la arquitectura
gráfica.
! Avanzado: Permite un reparto eficiente del problema sobre
muchos procesadores minimizando conflictos en el acceso
a memoria.
7

Ventajas de CUDA sobre la programación


GPGPU precursora
• La fase de aprendizaje resulta muy corta, ya que los
elementos a utilizar resultan familiares:
! Código: Apenas unas pocas extensiones a lenguajes
convencionales (C, por ejemplo).
! 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.
8

CUDA
1. Introducción
2. Arquitectura
3. Programación
4. Compilación, depuración y monitorización
5. Estrategias de mejora
6. Bibliografía
9

GPUs de Nvidia
Blackwell
Ampere
Hopper

Turing Ada
Lovelace
Volta
Rendimiento

Pascal

Maxwell
Kepler
Fermi
Tesla

2008 2010 2012 2014 2016 2018 2020 2022 2024


2020
10

El modelo hardware de CUDA:


Un conjunto de procesadores SIMD GPU

• La GPU consta de: Multiprocesador N

! N multiprocesadores, cada uno con M cores. Multiprocesador 2

• Paralelismo masivo: Multiprocesador 1

! Aplicado sobre miles de threads.

! Compartiendo datos a diferentes niveles.


Core 1 Core 2
… Core M Unidad de
Control

• Computación heterogénea, CPU y GPU:


! GPU: Intensiva en datos. Paralelismo de grano fino.

! CPU: Gestión y control. Paralelismo de grano grueso.

G80 GT200 GF100 GK110 GM200 GP100 GV100 TU102 GA102 GH100

Lanzamiento 2006 2008 2010 2012 2014 2016 2017 2018 2020 2022

N multiproces. 16 30 16 15 24 56 84 72 84 144

M cores
8 8 32 192 128 64 64 64 128 128

Total cores 128 240 512 2880 3072 3584 5376 4608 10752 18432
11

Arquitectura del sistema de memoria


Tipos de memoria:
12

Arquitectura del sistema de memoria


Tipos de memoria:
• Registros
- Dentro del chip
- Acceso rápido
- Acceso por hilo
- Cantidad limitada
- 32 bits
13

Arquitectura del sistema de memoria


Tipos de memoria:
• Registros
• Memoria local
- En la memoria de vídeo
- Acceso lento
- Acceso por hilo
- No actúa como caché
- Mayor capacidad
14

Arquitectura del sistema de memoria


Tipos de memoria:
• Registros
• Memoria local
• Memoria compartida
- Dentro del chip
- Acceso rápido
- Acceso por bloque de hilos
- Sincronización hilos bloque
15

Arquitectura del sistema de memoria


Tipos de memoria:
• Registros
• Memoria local
• Memoria compartida
• Memoria global
- En la memoria de vídeo
- Acceso lento
- Acceso todos los bloques de
hilos
- Sincronización todos los hilos
- No actúa como caché
16

Arquitectura del sistema de memoria


Tipos de memoria:
• Registros
• Memoria local
• Memoria compartida
• Memoria global
• Memoria de constantes
- En la memoria de vídeo
- Actúa como caché
- Acceso todos los hilos
- Sólo lectura
17

Arquitectura del sistema de memoria


Tipos de memoria:
• Registros
• Memoria local
• Memoria compartida
• Memoria global
• Memoria de constantes
• Memoria de texturas
- En la memoria de vídeo
- Actúa como caché
- Acceso todos los hilos
- Sólo lectura

+ varios niveles de caché


18

Arquitectura del sistema de memoria

20.000 2.000

Mejor rendimiento si el movimiento de los datos se


produce cerca de los elementos de proceso
19

Arquitectura de cada grupo o nodo básico


para la construcción de las distintas GPUs
• Se compone de un grupo de multiprocesadores de varios
cores o SPs (stream processors). Cada multiprocesador
dispone de una memoria compartida o caché casi tan
rápida como el banco de registros, y por la que pueden
comunicarse los threads pertenecientes al mismo.
• Computación de punto flotante:
! Para 32 bits (2006).
! Para 64 bits (2008).
20

Escalabilidad del modelo aumentando el


número de nodos
Dos nodos: GeForce 8400 Cuatro nodos: GeForce 8600
21

Escalabilidad del modelo: En 8 y 15 nodos


llegamos a las dos primeras generaciones
• El modelo de 8 nodos es el de la GPU G80 (1ª gener.).

• El modelo de 15 nodos es el de la GTX 200 (2ª gener.)


22

Evolución según el número de cores


23

Generaciones

Arquitectura
G80 GT200 Fermi Kepler Maxwell Pascal Volta Turing Ampere Hopper
GPU
Nombre GeForce
GTX 200 GF 110 GK110 GM200 GP100 GV100 TU102 GA102 GH100
comercial 8800
Año de
2006 2008 2010 2012 2014 2016 2017 2018 2020 2022
lanzamiento

Transistores
681 1400 3000 7100 8100 15300 21100 18600 28300 80000
(millones)

Número de
128 240 512 2880 3072 3584 5376 4608 + 576 10752+ 336 18432+576
cores
Planificadores
1 1 2 32 64 60 336 288 336 576
de warps

Shared 16 KB + 96 KB 64 KB
16 KB 16 KB 128KB
memory 48 KB 16 KB + 32
(config. 96KB 128KB 256KB
(o vice KB + 48 KB
Caché L1 Ninguna Ninguna 48 KB hasta 96KB)
versa)

Caché L2 Ninguna Ninguna 768 KB 768 KB 2048 KB 4096 KB 6 MB 5632 KB 6144KB 60MB
Corrección de
errores No No Sí Sí Sí Sí Sí Sí Sí Sí
(DRAM)
24

La jerarquía de memoria
Fermi

• 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.
25

Jerarquía de memoria (Kepler)


• Shared memory/L1 (64KB):
! 48KB SM + 16KB L1
! 16KB SM + 48KB L1
! 32KB SM + 32KB L1
• Caché L1 más rápida que L2
• 48 KB caché sólo lectura
! No requiere accesos alineados
! Manejada por el compilador
! Mayor ancho de banda para
soportar los fallos
! Usa caché de texturas,
transparente al programador
26

Paralelismo dinámico

• Un kernel puede lanzar otro kernel


• No hay tanta intervención de la CPU
27

Paralelismo dinámico
• Útil cuando la carga de trabajo (datos) no es conocida en tiempo de compilación
• Útil para lanzar programas recursivos

• El código de un kernel es ejecutado por todos los threads, y por tanto, un kernel
puede producir millones de lanzamientos de kernels.
! Hay que usar sentencias IF

• Los kernels “hijo” no pueden usar la memoria compartida de los kernels “padre”
! Fácil de implementar en hardware, pero difícil para el programador garantizar la
corrección del código
28

Evolución NVIDIA
• Streaming Multiprocessor (SM) 1.x en la Arquitectura Tesla
Ø 8 cores CUDA
Ø 2 Super Function Units (SFU)
• Doble unidades schedulers y dispatch
Ø 1 a 512 o 768 threads activos
• Registros (32k)
• 16 KB shared memory
• 2 operaciones por ciclo

• Streaming Multiprocessor (SM) 2.0 en la Arquitectura Fermi (GF1xx)


Ø 32 cores CUDA
Ø 4 Super Function Units (SFU)
• Doble unidades schedulers y dispatch
Ø 1 a 1536 threads activos
• Registros (32k)
• 64 KB shared memory / L1 cache
• 2 operaciones por ciclo
29

Resumen Evolución NVIDIA


• Streaming Multiprocessor (SMX) 3.0 en la Arquitectura Kepler
Ø 192 cores CUDA
Ø 8 cores CUDA DP
Ø 32 Super Function Units (SFU)
• 4 schedulers y 8 dispatchers
Ø 1 a 2048 threads activos
• Registros (32k)
• 64 KB shared memory / L1 cache
• 1 operación por ciclo

• Streaming Multiprocessor (SMM) 4.0 en la Arquitectura Maxwell


Ø 128 cores CUDA
Ø 4 cores CUDA DP
Ø 32 Super Function Units (SFU)
• 4 schedulers y 8 dispatchers
Ø 1 a 2048 threads activos
• Registros (64k)
• 64 KB shared memory
• 24 KB L1 cache
• 1 operación por ciclo
30

Resumen Evolución NVIDIA


• Streaming Multiprocessor (SM) en la Arquitectura Pascal
Ø 64 cores CUDA SP
Ø 32 cores CUDA DP
Ø 16 Super Function Units (SFU)
• 2 schedulers y 4 dispatchers
Ø 1 a 2048 threads activos
• Registros (64k)
• 64 KB shared memory / L1 cache
• 1 operación por ciclo

• Streaming Multiprocessor (SM) en la Arquitectura Volta


Ø 64 cores CUDA SP
Ø 32 cores CUDA DP
Ø 4 Super Function Units (SFU)
• 4 schedulers y 4 dispatchers
Ø 1 a 2048 threads activos
• Registros (64k)
• 96 KB shared memory
31

Resumen Evolución NVIDIA


• Streaming Multiprocessor (SM) en la Arquitectura Turing
Ø 64 cores FP32
Ø 64 cores INT32
Ø 8 Tensor cores
Ø 1 RT core
• 4 schedulers+dispatchers
Ø 1 a 2048 threads activos
• Registros (64k)
• 96 KB shared memory / L1 cache

• Streaming Multiprocessor (SM) en la Arquitectura Ampere


Ø 64 cores FP32
Ø 64 cores INT32 + FP32
Ø 8 Tensor cores
Ø 1 RT core
• 4 schedulers y 4 dispatchers
Ø 1 a 2048 threads activos
• Registros (64k)
• 128 KB shared memory / L1 cache
32

Resumen Evolución NVIDIA


• Streaming Multiprocessor (SM) en la Arquitectura Hopper
Ø 128 cores FP32
Ø 64 cores FP64
Ø 64 cores INT32
Ø 4 Tensor cores
• 4 schedulers+dispatchers
Ø 1 a 2048 threads activos
• Registros (64k)
• 256 KB shared memory / L1 cache
33

CUDA
1. Introducción
2. Arquitectura
3. Programación
4. Compilación, depuración y monitorización
5. Estrategias de mejora
6. Bibliografía
34

El modelo de programación CUDA

• La GPU (device) ofrece a la CPU (host) la visión de un


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

• Los threads de CUDA son extremadamente ligeros.


! Se crean en un tiempo muy efímero.
! El cambio de contexto es inmediato.
• Objetivo del programador: Declarar miles de threads, que la
GPU necesita para lograr rendimiento y escalabilidad.
35

Términos CUDA

Los programadores se enfrentan al reto de exponer el paralelismo para


múltiples cores y para múltiples threads por core. Para ello, deben usar
los siguientes elementos:

• Device = GPU = conjunto de multiprocesadores.


• Multiprocesador = conjunto de procesadores y memoria.
• Kernel = programa ejecutándose en GPU.
• Grid = matriz de bloques de threads que ejecutan un kernel.
• Bloque de threads (thread block) = grupo de threads que ejecutan
un kernel delimitando su dominio computacional según su ID, y que
pueden comunicarse a través de la memoria compartida del
multiprocesador.
36

Estructura de una aplicación CUDA


Código aplicación

Resto del código


secuencial
Funciones de cómputo
GPU intensivo CPU
Uso de GPU para
paralelizar

+
37

Estructura de una aplicación CUDA

Host Thread Grid of Thread Blocks

...

GPU

SMem

SMem

SMem
CPU

Cache Cache

Host
Device Memory
Memory
38

Estructura de un programa CUDA


• Cada multiprocesador procesa lotes de bloques de
threads, uno detrás de otro
! Bloques activos = los bloques procesados por un
multiprocesador en un lote.
! Threads 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 threads 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.
39

Recursos según la GPU usada para programar CUDA

Parámetro
Valor según generación de GPU

CUDA Compute Capabilities Fermi Kepler Maxwell Pascal Volta Turing Ampere
1.0 y 1.1 1.2 y 1.3
2.0 y 2.1 3.0 - 3.7 5.0 - 5.3 6.0 - 6.2 7.0 7.5 8.0

Multiprocesadores / GPU 16 30 16 8-15 16 60 84 72 84

Cores / Multiprocesador 8 8 32 192 128 64 64 64 128

Threads / Warp 32 32 32 32 32 32 32 32 32

Bloques de threads / Multiprocesador 8 8 8 16 32 32 32 16 32

Threads / Bloque 512 512 1024 1024 1024 1024 1024 1024 1024

Threads / Multiprocesador 768 1024 1536 2048 2048 2048 2048 1024 2048

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

16KB
16KB
Memoria compartida / Multiprocesador 16KB 16KB 32KB 96KB 64KB 96KB 64K 164K
48KB
48KB
40

Planificación de instrucciones:
Bloques de hilos ejemplo, CCC 1.0, 1.1)
B1

· · ···· Registros
Bn · Máximo Asignación
Máximo ··· ·512 hilos a un multiproc. Memoria compartida

8 bloques Core
········ Core
Core Core
···· SFU SFU
Core Core

Core Core

Máximo 768 hilos

• 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 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.
41

Planificación

• Cada bloque sólo puede ejecutarse en un SM


• Threads de un bloque pueden compartir datos usando Shared Memory
• Threads en diferentes SMs se comunican usando Global Memory
42

CPU (host) GPU (device)


WARPs. Concepto Kernel 1 Grid 1

Bloque Bloque Bloque


(0, 0) (1, 0) (2, 0)

Bloque Bloque Bloque


(0, 1) (1, 1) (2, 1)

Grid 2
Kernel 2

Bloque (1, 0)

Warp 0 Warp 1
Hilo … Hilo Hilo … Hilo
(0, 0) (31, 0) (32, 0) (63, 0)

32 threads SM Warp 2 Warp 3


Hilo … Hilo Hilo … Hilo
... = 32 threads
(0, 1) (31, 1) (32, 1) (63, 1)

Warp 4 Warp 5
32 threads Hilo … Hilo Hilo … Hilo
(0, 2) (31, 2) (32, 2) (63, 2)
Bloque de Warps
threads
43

WARPs. Ejecución
• Si 3 bloques de threads se
asignan a un mismo …
WARPS del bloque 1

WARPS del bloque 2

t0 t1 t2 … t31 t0 t1 t2 … t31
multiprocesador y cada uno de … …
estos bloques tiene 256
threads, ¿Cuántos warps hay
en ese multiprocesador? Streaming Multiprocessor

! Cada bloque tendrá 8 warps. Instruction L1 Data L1

Instruction Fetch/Dispatch
! Habrá 8 * 3 = 24 warps.
Shared Memory
! En un instante concreto, sólo uno
SP SP
de esos 24 warps estará
ejecutándose físicamente en el SP
SFU
SP
SFU
hardware del multiprocesador. SP SP

SP SP
44

Multiprocesador
Warps - Planificación

Warp 5 Warp 23 Warp 5 Warp 12 Warp 12 Warp 11 Warp 15


Instr. 10 Instr. 17 Instr. 11 Instr. 3 Instr. 4 Instr. 8 Instr. 1

ciclos

• El warp es la unidad de planificación. Se usa:


! 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.
• El cambio de contexto entre warps de un multiprocesador
se lleva a cabo sin penalidad en ciclos de ejecución.
45

Warps - Planificación

• Cuando acaba un warp, los


candidatos a ser ejecutados a
continuación se eligen de entre la Planificador de WARPS
del multiprocesador
cola de warp disponibles.
tiempo
• Todos los threads del warp
warp 8 instrucción 11
ejecutan la misma instrucción.
• Si se realiza un acceso a memoria warp 1 instrucción 42
global cada 4 instrucciones, se
warp 3 instrucción 95
necesita un mínimo de 13 warps ..
intercalados para ocultar la .
warp 8 instrucción 12
latencia a memoria de 200 ciclos.
(4 ciclos ejecutar una instrucción) warp 3 instrucción 96
46

Ocultar la latencia

• El objetivo de la planificación es ocultar la latencia, que en


el mejor de los casos llevaría a una ocupación de los
recursos del 100%

Thread 1

Thread 2

Thread 3

Thread 4

Ocupación

Cómputo Acceso memoria


47

Problema de la divergencia
if (in[i] == 0) out[i] = sin(x);
else out[i] = 2*x; warp

in[i] == 0 in[i] == 0

Tiempo
idle
out[i] = 2*x
out[i] = sin(x)
48

Problema de la divergencia
• Threads del mismo warp siguen caminos diferentes
! Sentencia if-then-else: unos toman el camino “then” y otros el camino “else”
! Bucles: unos realizan diferente número de iteraciones que otros
• Los diferentes caminos se serializan
! No todos los threads ejecutan esas sentencias a la vez
• Durante la ejecución de cada camino, los threads que lo toman se
ejecutan a la vez
• El número de caminos puede ser grande si hay sentencias de ese
tipo anidadas
• Puede aparecer si las condiciones de las sentencias están en
función del identificador del thread
! if (threadIdx.x > 2) { }
! threads 0, 1 y 2 siguen diferente camino que el resto
• Posible solución: la decisión en función del tamaño del warp, o
bloque
49

Kernels (y su relación con los threads)

• 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 (hasta 2ª gener.).
• Cuando el kernel finaliza, todos los recursos de la
GPU se liberan y quedan disponibles íntegramente
para el kernel siguiente.

§ Según su threadID, cada 0 1 2 3 4 5 6 7


threadID

thread:
§ Ejecuta el mismo código sobre un

float x = input[threadID];
área diferente de datos. float y = func(x);
output[threadID] = y;
§ Puede tomar decisiones de control …

para diferenciar su ejecución del


resto.
50

threads (y su relación con los bloques)

• La cooperación entre threads resulta muy valiosa:


! 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 threads 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.

Thread Block 0 Thread Block 1 Thread Block N-1


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


… … …
float x = input[threadID]; float x = input[threadID]; float x = input[threadID];
float y = func(x); float y = func(x); float y = func(x);
output[threadID] = y; output[threadID] = y; output[threadID] = y;
… … …
51

Particionamiento de computaciones y datos

• Un bloque de threads es un lote CPU (host) GPU (device)

de threads que pueden cooperar: Grid 1

! Compartiendo datos a través de Kernel 1


Bloque Bloque Bloque
memoria compartida. (0, 0) (1, 0) (2, 0)

! Sincronizando su ejecución para Bloque Bloque Bloque

acceder a memoria sin conflictos. (0, 1) (1, 1) (2, 1)

• Un kernel se ejecuta como una Grid 2

malla o grid 1D o 2D (1D, 2D y Kernel 2

3D desde kepler) de bloques de


threads 1D, 2D o 3D. Bloque (1, 1)
• Los threads y los bloques tienen
IDs para que cada thread pueda Hilo Hilo Hilo Hilo Hilo
(0, 0) (1, 0) (2, 0) (3, 0) (4, 0)
acotar sobre qué datos trabaja, y Hilo Hilo Hilo Hilo Hilo
simplificar el direccionamiento a (0, 1) (1, 1) (2, 1) (3, 1) (4, 1)

memoria al procesar datos Hilo Hilo Hilo Hilo Hilo


(0, 2) (1, 2) (2, 2) (3, 2) (4, 2)
multidimensionales
52

Manipulación de datos
• 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 memoria compartida y la
caché de sólo lectura.
• La caché es mucho más pequeña en la GPU, por lo que el programador
debe explotar al máximo la localidad.

Memoria Ubicación Caché Acceso Ámbito Declaración Vigencia


Local Off-chip No Lect./escr. Un thread __device__ Thread
Compartida On-chip - Lect./escr. Threads de un bloque __shared__ Bloque
Global Off-chip No Lect./escr. Los threads y la CPU __global__ Kernel
Constantes Off-chip Sí Lectura Los threads y la CPU __constant__ Kernel
De texturas Off-chip Sí Lectura Los threads y la CPU __texture__ Kernel
53

Principales debilidades/riesgos de CUDA


• 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.
• Limitada capacidad del banco de registros y la memoria
compartida que comparten todos los threads 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.
54

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
threads. Recordar que el cambio de contexto es gratis en
CUDA.
1. Si los threads de un mismo bloque necesitan comunicarse, utilizar
la memoria compartida y __syncthreads()
2. Si los threads 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.
55

Ejemplo 1: suma de vectores

vector A A[0] A[1] A[2] … A[N-1]

vector B B[0] B[1] B[2] … B[N-1]

+ + + +

vector C C[0] C[1] C[2] … C[N-1]


56

Ejemplo 1: suma de vectores


Código C tradicional

// Suma de vectores: C = A + B
void vecAdd(float *h_A, float *h_B, float *h_C, int n)
{
int i;
for (i = 0; i<n; i++)
h_C[i] = h_A[i] + h_B[i];
}

int main()
{
// Asignación de memoria para h_A, h_B y h_C
// Lectura de datos para h_A y h_B, N elementos

vecAdd(h_A, h_B, h_C, N);
}
57

Ejemplo 1: suma de vectores


CPU + GPU

int size = n* sizeof(float);


float *d_A, *d_B, *d_C;

Parte 1
// Parte 1
// Asignar memoria en el dispositivo para A, B y C
Parte 2 // Copiar A y B a la memoria del dispositivo

CPU GPU
// Parte 2
// Lanzar el kernel – se realiza la suma de los vectores
Parte 3
// Parte 3
// Copiar C desde la memoria del dispositivo a la del Host
// Liberar memoria de A, B y C en el dispositivo
58

Ejemplo 1: suma de vectores


CPU + GPU

(Device) Grid
Block (0, 0) Block (0, 1)
cudaMalloc()
Registros Registros Registros Registros
Reservar memoria
Thread (0, 0) Thread (0, 1) Thread (0, 0) Thread (0, 1)
en el dispositivo
Host

Memoria
Global

cudaFree()

cudaMemcpy() Liberar memoria


en el dispositivo
Transferencias
entre Host y
dispositivo
59

Ejemplo 1: suma de vectores


CPU + GPU
int size = n * sizeof(float);
float *d_A, *d_B, *d_C;

Parte 1 // Parte 1
// Asignar memoria en el dispositivo para A, B y C
cudaMalloc((void **) &d_A, size);
Parte 2 cudaMalloc((void **) &d_B, size);
cudaMalloc((void **) &d_C, size);
CPU GPU
// Copiar A y B a la memoria del dispositivo
cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);
Parte 3
// Parte 2
// Lanzar el kernel – se realiza la suma de los vectores
Se verá más adelante
// Parte 3
// Copiar C desde la memoria del dispositivo a la del Host
cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);

// Liberar memoria de A, B y C en el dispositivo


cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
60

Lista de extensiones sobre el lenguaje C


¡ Modificadores para las
declaraciones: __device__ float array[N];
¡ global, device, shared, local, __global__ void convolve(float *image) {
constant.
__shared__ float region[M];
¡ Variables y funciones ...
intrínsecas: region[threadIdx.x] = image[i];
¡ threadIdx, blockIdx,
__syncthreads __syncthreads();
...
¡ Runtime API. image[j] = result;
}
¡ Memoria, gestión de la
ejecución. // Alojar memoria en la GPU
void *myimage;
cudaMalloc(&myimage, bytes);

¡ Funciones kernel para lanzar // 100 bloques de threads, 10 threads en cada bloque
código a la GPU (device) desde convolve<<<100, 10>>> (myimage);
la CPU (host).
61

La interacción entre la CPU y la GPU


• CUDA extiende el lenguaje C con un nuevo tipo de función,
kernel, que ejecutan en paralelo los threads 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 (hasta paralelismo
dinámico).
• Cada kernel GPU tiene una barrera implícita a su conclusión, es
decir, no finaliza hasta que no lo hagan todos sus threads.
• Aprovecharemos al máximo el biprocesador CPU-GPU si les
vamos intercalando código con similar carga computacional.
62

La interacción entre la CPU y la GPU


__global__ kernelA(){···}
__global__ kernelB(){···}
int main()
CPU
···
Ejecución

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


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

Un kernel no comienza su ejecución en GPU hasta que no hayan finalizado


todas las llamadas CUDA anteriores (1ª-2ª gen.).
63

Modificadores para las funciones y


lanzamiento de ejecuciones en GPU
• Modificadores para las funciones ejecutadas en la GPU:
! __global__ void MyKernel() { } // Invocado por la CPU
! __device__ float MyFunc() { } // Invocado por la GPU

• Modificadores para las variables que residen en la GPU:


! __shared__ float MySharedArray[32]; // En mem. compa.
! __constant__ float MyConstantArray[32];

• Configuración de la ejecución para lanzar kernels:


! dim2 gridDim(100,50); // 5000 bloques de threads
! dim3 blockDim(4,8,8); // 256 threads por bloque
! MyKernel <<< gridDim,blockDim >>> (pars.); // Lanzam.
64

Variables y funciones intrínsecas

! dim3 gridDim; // Dimensión del grid


! dim3 blockDim; // Dimensión del bloque

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


! uint3 threadIdx; // Indice del thread dentro del bloque

! void __syncthreads(); // Sincronización entre threads

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.
65

Funciones para conocer en tiempo de


ejecución con qué recursos contamos
• 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:
! cudaGetDeviceCount(int* count);
• Para conocer los recursos disponibles en la GPU (caché, registros,
frecuencia de reloj, ...):
! cudaGetDeviceProperties(struct cudaDeviceProp* prop, int dev);
• Para conocer la mejor GPU que reúne 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 código:
! cudaGetDevice(int* dev);
66

Para gestionar la memoria de vídeo


• Para reservar y liberar memoria en la GPU:
! cudaMalloc(void* p, size_t numBytes) 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:
! 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);
67

Ejemplo 2: Descripción
• Reservar espacio para n enteros en la memoria de la CPU.
• Reservar espacio para n enteros en la memoria de la GPU.
• Inicializar la memoria reservada de la GPU a cero.
• Copiar los valores desde la GPU a la CPU.
• Imprimir los valores.
• Liberar el espacio de memoria en la GPU.
• Liberar el espacio de memoria en la CPU.
68

Ejemplo 2: Implementación
int main()
{
int dimx = 16;
int num_bytes = dimx*sizeof(int);
int *d_a=0, *h_a=0; // device and host pointers

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 );
}
69

Transferencias de memoria asíncronas


• Las llamadas a cudaMemcpy() son síncronas,
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 comunicación y computación.
! En la sección “Estrategias de mejora” pondremos un ejemplo.
70

Ejemplo 3: Incrementar un valor “b”


a los N elementos de un vector

Programa C en CPU Programa CUDA en GPU

__global__ void increment_gpu(float *a, float b, int N)


void increment_cpu(float *a, float b, int N)
{
{ int idx = blockIdx.x * blockDim.x + threadIdx.x;
for (int idx = 0; idx<N; idx++) if (idx < N)
a[idx] = a[idx] + b; a[idx] = a[idx] + b;
} }

void main()
{
void main() …..
{
dim3 dimBlock (blocksize);
..... dim3 dimGrid( ceil( N / (float)blocksize) );
increment_cpu(a, b, N); increment_gpu<<<dimGrid, dimBlock>>>(a, b, N);
…..
…..
} }
71

Ejemplo 3: Incrementar un valor “b”


a los N elementos de un vector

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


encargándose cada thread de computar un elemento del vector.
Extensiones

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


al lenguaje

blockDim.x=4 blockDim.x=4 blockDim.x=4 blockDim.x=4


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

Patrón de acceso
int idx = (blockId.x * blockDim.x) + threadIdx.x; común
Se mapeará del índice local threadIdx al índice global
Nota: blockDim debería ser >= 32 (warp size) en código real, esto es sólo un ejemplo
72

Código en CPU para el ejemplo 3


(azul es C, verde es CUDA)

// aloja memoria en la CPU


unsigned int numBytes = N * sizeof(float);
float* h_A = (float*) malloc(numBytes);

// aloja memoria en la GPU


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

// copia los datos de la CPU a la GPU


cudaMemcpy(d_A, h_A, numBytes, cudaMemcpyHostToDevice);

// ejecuta el kernel.
increment_gpu <<< N/blockSize, blockSize >>> (d_A, b, N);

// copia los datos de regreso a la CPU


cudaMemcpy(h_A, d_A, numBytes, cudaMemcpyDeviceToHost);

// libera la memoria de vídeo


cudaFree(d_A);
73

Ejemplo 1: suma de vectores

0 1 2 254 255

i = blockIdx.x * blockDim.x + threadIdx.x;


C[i] = A[i] + B[i];

__global__ void vecAddKernel (float* A, float* B, float* C, int n)


{
int i = blockDim.x*blockIdx.x + threadIdx.x;
if (i<n) C[i] = A[i] + B[i];
}
74

Ejemplo 1: suma de vectores

Thread Block 0 Thread Block 1 Thread Block N-1


0 1 2 254 255 0 1 2 254 255 0 1 2 254 255
… … …
i = blockIdx.x * blockDim.x + i = blockIdx.x * blockDim.x + i = blockIdx.x * blockDim.x +

threadIdx.x; threadIdx.x; threadIdx.x;
C[i] = A[i] + B[i]; C[i] = A[i] + B[i]; C[i] = A[i] + B[i];

… … …

// Parte 2
// Lanzar el kernel – se realiza la suma de los vectores
// Lanza ceil(n/256.0) bloques de 256 threads cada uno
vecAddKernel<<<ceil(n/256.0),256>>>(d_A, d_B, d_C, n);

// Se obtiene el mismo resultado de esta otra forma:


dim3 DimGrid((n-1)/256 + 1, 1, 1);
dim3 DimBlock(256, 1, 1);
vecAddKernel<<<DimGrid,DimBlock>>>(d_A, d_B, d_C, n);
75

Ejemplo 1: suma de vectores

__global__ void vecAddKernel (float* A, float* B, float* C, int n)


{
int i = blockDim.x*blockIdx.x + threadIdx.x;
if (i<n) C[i] = A[i] + B[i];
}

Análisis de divergencia
• n = 1000 y bloques de 256 threads
• 4 bloques de 256 threads y 8 warps/bloque
• Para los warps de los bloques 0, 1 y 2 (24 warps) no hay divergencia
• Para los warps 0 al 6 del bloque 3 no hay divergencia
• Threads 992 al 999 tomarán el camino “then”, el resto no
• El efecto en este caso es pequeño: 1 de 32 warps tienen divergencia (~ 3%)
76

Ejemplo 4: SAXPY

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
}
// Invocar al kernel SAXPY secuencial
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];
}
// Invocar al kernel SAXPY paralelo con 256 threads/bloque
int nblocks = (n + 255) / 256;
saxpy_parallel<<<nblocks, 256>>>(n, 2.0, x, y);
77

Ejemplo 2D: Suma de matrices


Matrices A, B y C
• Para cada uno de los 4x4 bloques del grid: de 16x16 elementos

! BlockIdx: vector (1D, 2D o 3D) que identifica


Las tres matrices se
el bloque dentro del grid. particionan igual,
otorgando un elemento
a cada thread

• Para cada uno de los 4x4 threads del bloque:


¿Qué thread computa C[2,15]?
! ThreadIdx: vector (1D, 2D o 3D) que blockIdx = (3,0)
identifica el thread dentro de su bloque. threadIdx = (3,2)

BlockDim.x BlockIdx.x
__global__ void matAdd (float A[N][N],float B[N][N],float es 3
BlockIdx.y ····
···· ····
···· ····
···· ····
····
C[N][N]) es 0 ····
···· ····
···· ····
···· ····
····
{ ····
···· ····
···· ····
···· ····
····
····
···· ····
···· ····
···· ····
····
int j = blockIdx.x*blockDim.x + threadIdx.x; ····
···· ····
···· ····
···· ····
····
····
···· ····
···· ····
···· ····
····

blockDim.y
int i = blockIdx.y*blockDim.y + threadIdx.y; ····
···· ····
···· ····
···· ····
····
····
···· ····
···· ····
···· ····
····
C[i][j] = A[i][j] + B[i][j];
{ Grid de
int main(){ bloques
dim3 dimBlock(4,4);
dim3 dimGrid (N/dimBlock.x, N/dimBlock.y);
matAdd <<< dimGrid, dimBlock >>> (A, B, C);
}
78

Ejemplo 2D: Multiplicación de matrices


Implementación secuencial

void MatMul-Seq (float *A, float *B, float *C)


{
unsigned int i, j, k;
float sum;

for (i=0; i<Cheight; i++)


{
for (j=0; j<Cwidth; j++)
{
sum = 0;
for (k=0; k<Awidth; k++)
sum = sum + A[i*Awidth + k]*B[k*Bwidth + j];
C[i*Cwidth + j] = sum;
}
}
}

Un thread calcula toda la matriz 78


79

Ejemplo 2D: Multiplicación de matrices


Implementación CUDA del kernel

__global__ void MatMul-Cuda (float *A, float *B, float *C)


{
float sum = 0;
int i, j, k;

i = blockIdx.y * blockDim.y + threadIdx.y;


j = blockIdx.x * blockDim.x + threadIdx.x;

for (k = 0; k < Awidth; ++k)


sum = sum + A[i * Awidth + k] * B[k * Bwidth + j];

C[i * Cwidth + j] = sum;


}
80

CUDA
1. Introducción
2. Arquitectura
3. Programación
4. Compilación, depuración y monitorización
5. Estrategias de mejora
6. Bibliografía
81

El proceso de compilación

void funcion_en_CPU(… )
{ Kernels CUDA Resto del
...
} código C
void otras_funcs_CPU(int ...) {
...
}

void saxpy_serial(float ... ) {


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

Los diferentes módulos de compilación


• El código fuente CUDA se C/C++ CUDA Código
Application fuente
compila con NVCC.
! NVCC separa el código
que se ejecuta en CPU
NVCC Código CPU
del que lo hace en GPU.
• La compilación se realiza Virtual
en dos etapas: PTX Code
! Virtual: Genera código
PTX (Parallel Thread
eXecution). Físico
PTX to Target
! Física: Genera el binario Compiler
para una GPU específica
(o incluso para una CPU
multicore). Código
G80 … GPU objeto
83

NVCC (Nvidia CUDA Compiler)

• NVCC es un driver del compilador.


! Funciona invocando todos los compiladores y
herramientas necesarias como cudacc, g++, cl,
...
• NVCC produce como salida:
! Código C para la CPU, que debe luego compilarse con el
resto de la aplicación utilizando otra herramienta.
! Código objeto PTX para la GPU.
• El ejecutable CUDA usa dos librerías dinámicas:
! The CUDA runtime library (cudart)
! The CUDA core library (cuda)
84

Depuración

NSIGHT CUDA-GDB CUDA MEMCHECK

Proporcionados por NVIDIA

Proporcionados por otros

https://developer.nvidia.com/debugging-solutions
85

Monitorización

NSIGHT NVVP NVPROF

Proporcionados por NVIDIA

VampirTrace
TAU

Proporcionados por otros

https://developer.nvidia.com/performance-analysis-tools
86

CUDA
1. Introducción
2. Arquitectura
3. Programación
4. Compilación, depuración y monitorización
5. Estrategias de mejora
6. Bibliografía
87

1. Solapar computación y comunicación


• Posibilidades:
! Solapar una transferencia de datos a la GPU con la computación en CPU de
una función que se invoca justo a continuación. Es posible en cualquier
dispositivo CUDA, aprovechando que el lanzamiento de kernels desde la
CPU es asíncrono.
! Solapar una transferencia de datos CPU ↔ GPU con la computación de un
kernel en GPU. Sólo es posible a partir de CUDA Compute Cap. 1.1. Más
complejo de implementar, pues necesitamos tres cosas:
1. Alojar con cudaMallocHost() memoria pinned en CPU (para que no pagine).
2. Definir streams (listas de operaciones CUDA que se ejecutan
secuencialmente).
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, para desde ahí lanzar el kernel que se
computará en GPU en paralelo con la transferencia entre CPU y GPU
88

1. Solapar computación y comunicación


• En los ejemplos, la forma en la que se usa cudaMemcpy serializa la
transferencia de datos y la computación

Trans. A Trans. B Comp Trans. C

tiempo

Sólo una dirección, PCIe ocioso Sólo una


GPU ociosa dirección, GPU
ociosa
89

1. Solapar computación y comunicación

• Dividir una transferencia grande en varias


• Solapar la computación y transferencia de partes consecutivas

Suma de vectores

Trans Trans Comp Trans


A.0 B.0 C.0 = A.0 + B.0 C.0

Trans Trans Comp Trans


A.1 B.1 C.1 = A.1 + B.1 C.1

Trans Trans Comp


A.2 B.2 C.2 = A.2 + B.2

Trans Trans
A.3 B.3
90

1. Solapar computación y comunicación

Streams
• Las peticiones (transferencias y kernels) desde el host son puestas
primero, y completadas después, en orden en una cola FIFO
• Para permitir solapamiento se usan varias colas (streams)

host thread host thread

cudaMemcpy()
FIFO Stream 0 Stream 1
kernel
synchronize
Event
cudaMemcpy()
91

1. Solapar computación y comunicación

cudaStream_t stream0, stream1;


cudaStreamCreate(&stream0);
cudaStreamCreate(&stream1);

for (int i=0; i<n; i+=SegSize*2) {


cudaMemcpyAsync(d_A0, h_A+i, SegSize*sizeof(float),…, stream0);
cudaMemcpyAsync(d_B0, h_B+i, SegSize*sizeof(float),…, stream0);
vecAddKernel <<<SegSize/256, 256, 0, stream0>>>(d_A0, d_B0,…);
cudaMemcpyAsync(h_C+i, d_C0, SegSize*sizeof(float),…, stream0);

cudaMemcpyAsync(d_A1, h_A+i+SegSize, SegSize*sizeof(float),…, stream1);


cudaMemcpyAsync(d_B1, h_B+i+SegSize, SegSize*sizeof(float),…, stream1);
vecAddKernel <<<SegSize/256, 256, 0, stream1>>>(d_A1, d_B1, …);
cudaMemcpyAsync(d_C1, h_C+i+SegSize, SegSize*sizeof(float),…, stream1);
}
92

1. Solapar computación y comunicación

• El resultado no es demasiado bueno


• La transferencia C.0 bloquea a las transferencias A.1 y B.1

Trans Trans Comp Trans


A.0 B.0 C.0 = A.0 + B.0 C.0

Trans Trans Comp Trans


A.1 B.1 C.1= A.1 + B.1 C.1
93

1. Solapar computación y comunicación

cudaStream_t stream0, stream1;


cudaStreamCreate(&stream0);
cudaStreamCreate(&stream1);

for (int i=0; i<n; i+=SegSize*2) {


cudaMemcpyAsync(d_A0, h_A+i, SegSize*sizeof(float),…, stream0);
cudaMemcpyAsync(d_B0, h_B+i, SegSize*sizeof(float),…, stream0);
cudaMemcpyAsync(d_A1, h_A+i+SegSize, SegSize*sizeof(float),…, stream1);
cudaMemcpyAsync(d_B1, h_B+i+SegSize, SegSize*sizeof(float),…, stream1);

vecAddKernel <<<SegSize/256, 256, 0, stream0>>>(d_A0, d_B0, …);


vecAddKernel <<<SegSize/256, 256, 0, stream1>>>(d_A1, d_B1, …);

cudaMemcpyAsync(h_C+i, d_C0, SegSize*sizeof(float),…, stream0);


cudaMemcpyAsync(h_C+i+SegSize, d_C1, SegSize*sizeof(float),…, stream1);
}
94

1. Solapar computación y comunicación

• El resultado es mejor
• La transferencia C.1 bloquea a las transferencias A.2 y B.2 de la
siguiente iteración

Trans Trans Comp Trans


A.0 B.0 C.0 = A.0 + B.0 C.0 Iteración n

Trans Trans Comp Trans


A.1 B.1 C.1= A.1 + B.1 C.1

Trans Trans Comp


Iteración n+1 A.2 B.2 C.2 = A.2
+

Trans
A.2

• Para obtener la situación ideal, el código es más complicado


95

1. Solapar computación y comunicación

Múltiples GPUs
GpuNum = 0;

cudaGetNumDevices(&GpuNum);
...
for (intd= 0; d < GpuNum; d++)
{
cudaSetDevice(d);
. . .
cudaMalloc();
cudaMemcpy();
kernel<<<blocks, threads>>>(args);
cudaMemcpy();
. . .
}
96

2. Optimizar el uso de la memoria


• Minimizar las transferencias entre CPU y GPU.
! Ya que este ancho de banda es muy inferior al de la memoria de vídeo.
Por ejemplo, según una Fermi (GeForce GTX 480):
– Host to Device Bandwidth: 2,25 GB/sg.
– Device to Host Bandwidth: 2,00 GB/sg.
– Device to Device Bandwidth: 117,83 GB/sg.
! Si se quiere aumentar el ancho de banda, usar memoria “pinned” (sin
abusar), que aprovecha mejor PCI-express
! Agrupar las transferencias de datos entre CPU y GPU
! Ya que la latencia predomina en el coste sobre el ancho de banda.
• 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.
97

3. Optimizar los patrones de acceso a memoria


• El ancho de banda efectivo puede variar un orden de
magnitud dependiendo del patrón de acceso si sabemos
utilizar las siguientes armas:
! Accesos coalescentes a memoria global (menos importante desde Fermi).
! Accesos a memoria compartida sin conflictos a sus bancos.
! Accesos a memoria de texturas (que pasa por caché).
! Accesos a memoria de constantes que tienen una misma dirección.
• Recordar que:
! 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.
98

Memoria global
• Latencia alta
! Usarla lo menos posible
• Accesos por half-warp (16 threads), o warp completo.
! Intentar completarlos en el menor número de transacciones posible
(coalescing)

1 transacción 16 transacciones
99

Memoria global
• Latencia alta
! Usarla lo menos posible
• Accesos por half-warp (16 threads), o warp completo.
! Intentar completarlos en el menor número de transacciones posible
(coalescing)

matriz memoria global matriz memoria global

1 1
1 2 3 4 2 1 2 3 4 2

5 6 7 8 3 5 6 7 8 3
4 4
9 10 11 12 5 9 10 11 12 5
6 6
13 14 15 16 13 14 15 16
7 7
8 8

… …
100

Accesos coalescentes a memoria global


• Coalescing: Acceso coalescente o fusionado a memoria global
• Capacidad de la arquitectura para obtener 16 palabras de memoria
simultáneamente (en un único acceso) por medio de los 16 threads
del half-warp
• Reduce la latencia de memoria a 16 veces menos
• Idem 32 palabras
• Primera cuestión de eficiencia en el desarrollo del kernel
• Se consigue bajo ciertos patrones de acceso
• Estos patrones dependen de la capacidad de cómputo de la GPU
101

Visión de la memoria

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Sección 1 Sección 2 Sección 3 Sección 4

• El espacio de memoria es dividido en secciones


! Cuando una palabra es accedida, se recupera toda la sección

• Ejemplo básico: espacio de 16 bytes, secciones de 4 bytes


! En un caso más realista se tienen al menos 4GB, y secciones de 128 bytes o más
102

Accesos coalescentes

T0 T1 T2 T3 T0 T1 T2 T3

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Sección 1 Sección 2 Sección 3 Sección 4

Cuando todos los threads de un warp ejecutan una carga, si


todas las posiciones a las que se tiene que acceder caen en
la misma sección, sólo se necesita una petición a la
memoria para recuperar todos los datos, y el acceso es
totalmente coalescente
103

Accesos no coalescentes

T0 T1 T2 T3 T0 T1 T2 T3

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Sección 1 Sección 2 Sección 3 Sección 4

Cuando todos los threads de un warp ejecutan una carga, si


las posiciones a las que se tiene que acceder caen en
diferentes secciones, se necesitan varias peticiones a la
memoria para recuperar todos los datos, y el acceso no es
coalescente. Habrá datos recuperados que no se usen.
104

4. Usar la memoria compartida


Si el tamaño se conoce
Si el tamaño se desconoce
en tiempo de compilación
en tiempo de compilación

__global__ void kernel (...)


__global__ void kernel (...)
{
{
...
...
extern __shared__ float sData[];
__shared__ float sData[256];
...
...
}
}

void main()
void main()
{
{
...
...
smBytes = blockSize * sizeof(float);
kernel<<<nBlocks,blocksize>>>(...);
kernel<<<nBlocks,blocksize,smBytes>>>(...);
...
...
}
}
105

Ejemplo 2D: Multiplicación de matrices

__global__ void MatMul-Cuda (float *A, float *B, float *C)


{
float sum = 0;
int i, j, k;

i = blockIdx.y * blockDim.y + threadIdx.y;


j = blockIdx.x * blockDim.x + threadIdx.x;

for (k = 0; k < Awidth; ++k)


sum = sum + A[i * Awidth + k] * B[k * Bwidth + j];

C[i * Cwidth + j] = sum;


}
105
106

4. Usar la memoria compartida


• Dividida en módulos (bancos)
• En anteriores GPUs 16 bancos de 1KB, en más recientes 32 bancos
• Acceso simultáneo a los bancos
• Baja latencia (similar registros)
• Problema: conflictos en los bancos (acceso al mismo banco por dos o más threads)

• Cargar los datos desde la memoria global a la memoria compartida


• Sincronizar threads
• Procesar usando sólo memoria compartida
• Sincronizar
• Llevar resultados a memoria global
107

Multiplicación de matrices
threads

(0,0) (1,0) C0,0 C0,1


calculan B0,0 B0,1 B0,2 B0,3
(0,1) (1,1) C1,0 C1,1
B1,0 B1,1 B1,2 B1,3

B2,0 B2,1 B2,2 B2,3

B3,0 B3,1 B3,2 B3,3


cada thread lleva un dato de A y B

A0,0 A0,1 A0,2 A0,3 C0,0 C0,1 C0,2 C0,3


As Bs
A1,0 A1,1 A1,2 A1,3 C1,0 C1,1 C1,2 C1,3

A2,0 A2,1 A2,2 A2,3 C2,0 C2,1 C2,2 C2,3

A3,0 A3,1 A3,2 A3,3 C3,0 C3,1 C3,2 C3,3

memoria compartida 107


108

Multiplicación de matrices
threads

(0,0) (1,0) C0,0 C0,1


calculan B0,0 B0,1 B0,2 B0,3
(0,1) (1,1) C1,0 C1,1
B1,0 B1,1 B1,2 B1,3
C0,0 = A0,0 x B0,0 + A0,1 x B1,0
B2,0 B2,1 B2,2 B2,3
C0,1 = A0,0 x B0,1 + A0,1 x B1,1
C1,0 = A1,0 x B0,0 + A1,1 x B1,0 B3,0 B3,1 B3,2 B3,3
C1,1 = A1,0 x B0,1 + A1,1 x B1,1

A0,0 A0,1 A0,2 A0,3 C0,0 C0,1 C0,2 C0,3


As Bs
A1,0 A1,1 A1,2 A1,3 C1,0 C1,1 C1,2 C1,3
A0,0 A0,1 B0,0 B0,1 A2,0 A2,1 A2,2 A2,3 C2,0 C2,1 C2,2 C2,3
A1,0 A1,1 B1,0 B1,1 A3,0 A3,1 A3,2 A3,3 C3,0 C3,1 C3,2 C3,3

memoria compartida 108


109

Multiplicación de matrices
threads

(0,0) (1,0) C0,0 C0,1


calculan B0,0 B0,1 B0,2 B0,3
(0,1) (1,1) C1,0 C1,1
B1,0 B1,1 B1,2 B1,3
C0,0 = A0,0 x B0,0 + A0,1 x B1,0 + A0,2 x B2,0 + A0,3 x B3,0
B2,0 B2,1 B2,2 B2,3
C0,1 = A0,0 x B0,1 + A0,1 x B1,1 + A0,2 x B2,1 + A0,3 x B3,1
C1,0 = A1,0 x B0,0 + A1,1 x B1,0 + A1,2 x B2,0 + A1,3 x B3,0 B3,0 B3,1 B3,2 B3,3
C1,1 = A1,0 x B0,1 + A1,1 x B1,1 + A1,2 x B2,1 + A1,3 x B3,1

A0,0 A0,1 A0,2 A0,3 C0,0 C0,1 C0,2 C0,3


As Bs
A1,0 A1,1 A1,2 A1,3 C1,0 C1,1 C1,2 C1,3
A0,2 A0,3 B2,0 B2,1 A2,0 A2,1 A2,2 A2,3 C2,0 C2,1 C2,2 C2,3
A1,2 A1,3 B3,0 B3,1 A3,0 A3,1 A3,2 A3,3 C3,0 C3,1 C3,2 C3,3

memoria compartida 109


110

Multiplicación de matrices
Implementación CUDA del kernel – memoria compartida

__global__ void MatMul-Cuda-SM (float *A, float *B, float *C) for (t=0; t<Awidth/BLOCK_SIZE; ++t)
{ {
float sum = 0; As[ty][tx] = A[i * Awidth + (t * BLOCK_SIZE + tx)];
int i, j, k, t; Bs[ty][tx] = B[(t * BLOCK_SIZE + ty) * Bwidth + j];

int tx = threadIdx.x; __syncthreads();


int ty = threadIdx.y;
int bx = blockIdx.x; for (k = 0; k < BLOCK_SIZE; ++k)
int by = blockIdx.y; sum = sum + As[ty][k] * Bs[k][tx];

i = by * blockDim.y + ty; __syncthreads();


j = bx * blockDim.x + tx; }

__shared__ float As[BLOCK_SIZE][BLOCK_SIZE]; C[i * Cwidth + j] = sum;


__shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE]; }
111

Multiplicación de matrices

• Se usan todos los cores $Lt_0_7:


(1920 cores) x 1365 MHz = ld.global.f32 %f2, [%r15+0];
= 2620,8 Goperaciones/s ld.global.f32 %f3, [%r17+0];
mad.f32 %f1, %f2, %f3, %f1;
• De 8 operaciones sólo 2 son FLOP add.s32 %r17, %r17, 4096;
• Máxima productividad add.s32 %r15, %r15, 4;
! 2620,8 x ¼ = 655,2 GFLOP/S setp.ne.u32 %p1, %r15, %r16;
@%p1 bra $Lt_0_7;
• ¿Por qué no se llega a ese valor?

• Accesos a memoria
– ¼ operaciones son cargas
– (1920 cores) x (1365 MHz) x (¼ cargas) x (4bytes/carga) = 2.620 GB/s > 336 GB/s

El ancho de banda con memoria global no es suficiente

111
112

Multiplicación de matrices

• Bloque de threads à 16 x 16 = 256 threads


• Width = 4096 à 256 x 256 = 65536 bloques de threads

• Cada dato en una sub-matriz es leído por 16 threads


• Los accesos a memoria global se reducen en un factor 16 al usar
memoria compartida
• Se requiere ahora 2.620 / 16 ~ 164 GB/s < 336 GB/s

Ahora la memoria no es motivo para no alcanzar la productividad


deseada
113

5. Eliminar los conflictos en el acceso a


los bancos de memoria compartida
• Dividida en módulos (bancos)
• Caso de 16 bancos de 1KB
• Acceso simultáneo a los bancos
• Baja latencia (similar registros)
• Problema: conflictos en los bancos (acceso al mismo banco por dos o más threads)

Sin conflictos Sin conflictos Con conflictos


Thread 0 Banco 0 Thread 0 Banco 0 Thread 0 Banco 0

Thread 1 Banco 1 Thread 1 Banco 1 Thread 1 Banco 1

Thread 2 Banco 2 Thread 2 Banco 2 Thread 2 Banco 2

Thread 3 Banco 3 Thread 3 Banco 3 Thread 3 Banco 3

Thread 4 Banco 4 Thread 4 Banco 4 ... Banco 4

... ... Thread 8


... ... Banco 5

Thread 14 Banco 14 Thread 14 Banco 14 Thread 9 ...


...
Thread 15 Banco 15 Thread 15 Banco 15 Thread 15 Banco 15

Direccionamiento lineal Direccionamiento aleatorio Direccionamiento lineal


Stride = 1 Permutación 1:1 Stride = 2
114

CUDA
1. Introducción
2. Arquitectura
3. Programación
4. Compilación, depuración y monitorización
5. Estrategias de mejora
6. Bibliografía
115

Bibliografía
• CUDA Programming Guide. Las bases de CUDA.

• CUDA Best Practices Guide. Para optimizar código.

• CUDA Zone (http://www.nvidia.com/cuda).


! 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, cursos de programación paralela, ...

También podría gustarte