Documentos de Académico
Documentos de Profesional
Documentos de Cultura
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.
¿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:
• 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
...
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.
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
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
20.000 2.000
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
Paralelismo dinámico
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
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
Términos CUDA
+
37
...
GPU
SMem
SMem
SMem
CPU
Cache Cache
Host
Device Memory
Memory
38
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
Threads / Warp 32 32 32 32 32 32 32 32 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
Planificación
Grid 2
Kernel 2
Bloque (1, 0)
Warp 0 Warp 1
Hilo … Hilo Hilo … Hilo
(0, 0) (31, 0) (32, 0) (63, 0)
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
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
ciclos
Warps - Planificación
Ocultar la latencia
Thread 1
Thread 2
Thread 3
Thread 4
Ocupación
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
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 …
…
… … …
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
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.
+ + + +
// 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
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
(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()
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);
¡ 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
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 );
free( h_a );
cudaFree( d_a );
}
69
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
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
// ejecuta el kernel.
increment_gpu <<< N/blockSize, blockSize >>> (d_A, b, N);
0 1 2 254 255
…
… … …
// 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);
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
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
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 ...) {
...
}
Depuración
https://developer.nvidia.com/debugging-solutions
85
Monitorización
VampirTrace
TAU
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
tiempo
Suma de vectores
Trans Trans
A.3 B.3
90
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)
cudaMemcpy()
FIFO Stream 0 Stream 1
kernel
synchronize
Event
cudaMemcpy()
91
• El resultado es mejor
• La transferencia C.1 bloquea a las transferencias A.2 y B.2 de la
siguiente iteración
Trans
A.2
Múltiples GPUs
GpuNum = 0;
cudaGetNumDevices(&GpuNum);
...
for (intd= 0; d < GpuNum; d++)
{
cudaSetDevice(d);
. . .
cudaMalloc();
cudaMemcpy();
kernel<<<blocks, threads>>>(args);
cudaMemcpy();
. . .
}
96
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)
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
Visión de la memoria
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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
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
void main()
void main()
{
{
...
...
smBytes = blockSize * sizeof(float);
kernel<<<nBlocks,blocksize>>>(...);
kernel<<<nBlocks,blocksize,smBytes>>>(...);
...
...
}
}
105
Multiplicación de matrices
threads
Multiplicación de matrices
threads
Multiplicación de matrices
threads
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];
Multiplicación de matrices
• Accesos a memoria
– ¼ operaciones son cargas
– (1920 cores) x (1365 MHz) x (¼ cargas) x (4bytes/carga) = 2.620 GB/s > 336 GB/s
111
112
Multiplicación de matrices
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.