0% encontró este documento útil (0 votos)
26 vistas55 páginas

Clase 6 - ProgramacionCUDA2 - 2025

El documento aborda la programación masivamente paralela en GPUs utilizando CUDA, centrándose en el acceso coalesced a la memoria global y la memoria compartida. Se discuten conceptos como conflictos de bancos, errores en tiempo de ejecución y recomendaciones de rendimiento. Además, se presentan ejemplos prácticos para ilustrar el uso eficiente de la memoria en el contexto de CUDA.

Cargado por

Vale Diaz
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
26 vistas55 páginas

Clase 6 - ProgramacionCUDA2 - 2025

El documento aborda la programación masivamente paralela en GPUs utilizando CUDA, centrándose en el acceso coalesced a la memoria global y la memoria compartida. Se discuten conceptos como conflictos de bancos, errores en tiempo de ejecución y recomendaciones de rendimiento. Además, se presentan ejemplos prácticos para ilustrar el uso eficiente de la memoria en el contexto de CUDA.

Cargado por

Vale Diaz
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

Programación masivamente paralela en

procesadores gráficos (GPUs)

E. Dufrechou , M. Freire, P. Ezzatti y M. Pedemonte

Clase 6 – Programación CUDA II PMPenGPU


Clase 6
Programación CUDA II

Clase 6 – Programación CUDA II PMPenGPU


Contenido

• Acceso Coalesced a Memoria Global


• Memoria compartida
– Conflicto de bancos
– Tiling
• Errores en tiempo de ejecución
• Código PTX
• Algunas recomendaciones de performance

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

• El acceso a memoria global es por segmentos.


• Incluso cuando solamente se quiere leer una palabra.
• Si no se usan todos los datos de un segmento, se está
desperdiciando ancho de banda.
• Los segmentos están alineados a múltiplos de 128 bytes.
• El acceso no alineado es más costoso que el acceso alineado.
• Se desperdicia ancho de banda.

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

• Coalesced access:
– Según Merriam-Webster “to unite into a whole” (unir en un todo).
– Podríamos traducirlo como acceso unido o fusionado.
• Cada solicitud de acceso a memoria global de un warp:
– se puede partir en varias solicitudes
– cada solicitud es atendida (issued) independientemente.
• Los accesos a memoria de hilos de un warp se fusionan en una
o más transacciones según características que dependen de las
compute capability de la tarjeta.

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

• Desde compute capabilities 3.x:


– Las transacciones a memoria global son cacheadas.
– Hay un caché L1 para cada multiprocesador y un caché L2 compartido
por todos los multiprocesadores.
– Las caché lines son de 128 bytes y se mapean a segmentos alineados de
128 bytes de la memoria global.
– Los accesos a memoria caché en L1 y L2 usan transacciones de 128 bytes.
– Los accesos a memoria caché solamente en L2 usan transacciones de 32
bytes. Que los accesos sean solamente a L2 puede configurarse usando
modificadores en las instrucciones load y store.

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

• Desde compute capabilities 3.x:


– Si el tamaño de la palabra es de 8 bytes, se realizan dos solicitudes de
128 bytes, una para cada half-warp.
– Si el tamaño de la palabra es de 16 bytes, se realizan cuatro solicitudes
de 128 bytes, una para cada quarter-warp.
– Cada solicitud se particiona en cache-lines.
– Si se produce un miss, se accede a la memoria global.
– Los hilos pueden acceder a las palabras en cualquier orden, incluso a las
mismas palabras.

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

• El espacio de direcciones está particionado en segmentos


• Cuando se solicita una dirección de un segmento, se entregan los
datos correspondientes a todas las direcciones del segmento.
• Si todos los hilos del warp acceden al mismo segmento, se hace
una sola solicitud y se usan todos los datos.
• Cuando los accesos están distribuidos entre distintos segmentos:
– Se realizan múltiples solicitudes
– Hay datos a los que se accede y que se transfieren de la memoria a los
multiprocesadores que no son usados por los hilos
• Ejemplo de acceso de un warp serán a direcciones consecutivas:
– A[threadIdx.x]

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

• Ejemplo 1:
__global__ void CopiaOffset(float *output, float *input, int offset) {
int idx = blockIdx.x * blockDim.x + threadIdx.x + offset;
output[idx] = input[idx];
}

– Lanzando 10000 veces 320 bloques de 128 hilos en una 9800 GTX+
(Comp. Cap 1.1, 512 MB, 128 CUDA cores)

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

• Ejemplo 1:
__global__ void CopiaOffset(float *output, float *input, int offset) {
int idx = blockIdx.x * blockDim.x + threadIdx.x + offset;
output[idx] = input[idx];
}

– Lanzando 10000 veces 320 bloques de 128 hilos en una 480 (Comp. Cap
2.0, 1536 MB, 480 CUDA cores)
Offset GB/s
0 49.22
1 a 15 44.88
16 46.24
El pico teórico es 177 GB/s
17 a 31 44.88
32 49.22

Clase 6 – Programación CUDA II PMPenGPU


Acceso a Memoria Global

• Attenti al lupo:
– Cuando una instrucción no atómica ejecutada por un warp debe escribir
en la misma dirección de la memoria global para más de un hilo del warp
– Además del problema de race condition que se produce, de acuerdo al
funcionamiento de CUDA solamente un hilo realiza la escritura
– Está indefinido cual de los hilos!!!

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

• Ejemplo 2 (Robert Strzodka):

Array of Structs (AoS) Struct of Arrays (SoA)


struct NormalStruct { struct SoAContainer{
Type1 comp1; Type1 comp1[SIZE];
Type2 comp2; Type2 comp2[SIZE];
Type3 comp3; Type3 comp3[SIZE];
}; };

typedef NormalStruct SoAContainer container;


AoSContainer[SIZE];

AoSContainer container;

Clase 6 – Programación CUDA II PMPenGPU


Acceso Coalesced a Memoria Global

• Ejemplo 2 (Robert Strzodka):

Array of Structs (AoS): container[1].comp3;


container[2].comp3;
container[3].comp3;
container[4].comp3;

Struct of Arrays (SoA): container.comp3[1];


container.comp3[2];
container.comp3[3];
container.comp3[4];

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida

• Es cientos de veces más rápida (tanto en latencia como en


performance) que la memoria global.
• Hay una memoria compartida en cada multiprocesador.

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida

• Los contenidos y el uso de este tipo de memoria deben ser


explícitamente definidos por el usuario en el código del kernel.
• El alcance es a nivel del bloque de hilos.
• Por ello permite que los hilos de un mismo bloque puedan
cooperar.
• El tiempo de vida corresponde al tiempo de vida del bloque de
hilos.
• Es decir que el contenido se pierde cuando los hilos del bloque
finalizan su ejecución.

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida
• Para declarar la memoria compartida se puede usar una
constante:

#define CANT_HILOS 128

__global__ void reduction(float * output, float * input) {


__shared__ float compartida1[CANT_HILOS];
__shared__ float compartida2[CANT_HILOS];

};

reduction <<<N_BLOCK,CANT_HILOS>>> (output,input);

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida
• También puede ser una matriz:

#define TILE_WIDTH 128

__global__ void reduction(float * output, float * input) {


__shared__ float compartida[TILE_WIDTH][TILE_WIDTH];

};

reduction <<<N_BLOCK,CANT_HILOS>>> (output,input);

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida
• Para declarar la memoria compartida se puede hacer en forma
dinámica con extern:

__global__ void reduction(float * output, float * input) {


extern __shared__ float compartida[];

};

reduction <<<N_BLOCK,CANT_HILOS,
CANT_HILOS*sizeof(float)>>> (output,input);

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida
• También es posible “partir” el tamaño reservado cuando se usa
extern en varias estructuras:

__global__ void reduction(float * output, float * input) {


extern __shared__ float auxiliar[];

float* compartida1 = auxiliar;


float* compartida2 = (float*)&auxiliar[CANT_HILOS];

};

reduction <<<N_BLOCK,CANT_HILOS,
2*CANT_HILOS*sizeof(float)>>> (output,input);

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida

• La memoria compartida se divide en módulos del mismo


tamaño llamados bancos.
• En compute capabilities 3.X hay dos modos de
direccionamiento: de 32 bits y de 64 bits.
• En compute capabilities posteriores solamente hay
direccionamiento de 32 bits, por lo que para la explicación nos
centraremos en ese caso.

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida

• Palabras de 32 bits contiguas están en bancos contiguos.


• Los bancos pueden ser accedidos simultáneamente a nivel de
warp.
• Si las lecturas o escrituras caen todas en bancos distintos,
pueden ser atendidas simultáneamente.
• Cada banco puede atender una única solicitud por ciclo.
• En general, si dos solicitudes caen en el mismo banco de
memoria, se produce un conflicto (bank conflict) y el acceso al
banco es serializado:
– Lectura de palabras distintas
– Escrituras

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida

• No hay conflicto y el acceso es rápido cuando:


– Todos los hilos del warp acceden a diferentes bancos.
– Hilos del warp leen la misma palabra de un banco (broadcast).

• El acceso es más lento cuando:


– Varios hilos del warp acceden a palabras distintas del mismo banco.
– Se produce un conflicto y se debe serializar el acceso.
– Se requieren tantos ciclos como el máximo número de accesos al mismo
banco.

Clase 6 – Programación CUDA II PMPenGPU


Memoria Compartida

• Debido a que es más rápida que la memoria global suele usarse


como una especie de caché para reducir los accesos a memoria
global.
• También permite evitar accesos no coalesced a la memoria
global:
– Los datos se guardan en forma intermedia en la memoria compartida.
– Se reordena el acceso a los datos para que cuando se copien de memoria
compartida a memoria global el acceso sea coalesced.
• Esto se puede hacer de a pedazos si se debe cargar muchos
datos.
• En ese caso, se lo conoce con el nombre de tiling.

Clase 6 – Programación CUDA II PMPenGPU


Tiling

Clase 6 – Programación CUDA II PMPenGPU


Tiling

• Se divide el contenido de la memoria global en tiles (bloques?)


• Se concentra el cálculo de los hilos en un tile o un pequeño
número de tiles en cada momento.

Clase 6 – Programación CUDA II PMPenGPU


Tiling

• Se divide el contenido de la memoria global en tiles (bloques?)


• Se concentra el cálculo de los hilos en un tile o un pequeño
número de tiles en cada momento.

Clase 6 – Programación CUDA II PMPenGPU


Tiling

• Esquema general del Tiling:


– Identificar un tile de datos de memoria global que deban ser accedidos
por múltiples hilos.
– Cargar el tile desde memoria global a la memoria compartida.
– Utilizar los mecanismos de sincronización (__syncthreads()) para
asegurarse que todos los datos necesarios para el procesamiento están
cargados.
– Los hilos hacen los cálculos correspondientes accediendo a los datos en
la memoria compartida.
– Utilizar los mecanismos de sincronización para asegurarse que todos los
hilos completaron esta etapa del procesamiento.
– Se puede pasar a trabajar sobre el siguiente tile.

Clase 6 – Programación CUDA II PMPenGPU


Errores en tiempo de ejecución

Clase 6 – Programación CUDA II PMPenGPU


Errores en tiempo de ejecución

• CUDA no avisa cuando se produce un error.


• Veamos un ejemplo:
#include <cuda_runtime.h>

int main(int argc, char *argv[]){

float * inputGPU = NULL;


int size = 1000 * sizeof(float);
float * inputCPU = NULL;
inputCPU = (float*) malloc (size);

for (int j=0;j<1000;j++) {


inputCPU[j]=j+1.1f;
}

cudaMemcpy(inputGPU,inputCPU,size,cudaMemcpyHostToDevice);
cudaFree(inputGPU);
return 0;
• input
} no tiene memoria reservada y no da error!!!!
Clase 6 – Programación CUDA II PMPenGPU
Errores en tiempo de ejecución

• CUDA provee cuatro funciones para el manejo de errores:


• cudaError_t cudaGetLastError ():Devuelve el último
error de una invocación en tiempo de ejecución. Resetea el estado a
cudaSuccess.
• cudaError_t cudaPeekAtLastError():Devuelve el último
error de una invocación en tiempo de ejecución. NO resetea el estado
a cudaSuccess.
• char* cudaGetErrorName(cudaError_t error):Dado un
código de error devuelve la string que representa el error.
• char* cudaGetErrorString(cudaError_t error):Dado
un código de error devuelve la descripción del error.

Clase 6 – Programación CUDA II PMPenGPU


Errores en tiempo de ejecución
• Las invocaciones a operaciones como transferencias o reservas
y liberación de memoria ya devuelven un cudaError_t.
• Por lo que basta con usar la función cudaGetErrorString
para desplegar el error.
• Por ejemplo se puede definir:
#define CUDA_CHK(ans) { gpuAssert((ans), __FILE__, __LINE__); }

inline void gpuAssert(cudaError_t code, const char *file, int line,


bool abort=true){

if (code != cudaSuccess){
fprintf(stderr,"GPUassert: %s %s %d\n",cudaGetErrorString(code),
file, line);
if (abort) exit(code);
}
}

• Y se usa CUDA_CHK como wrapper de la invocación.


Clase 6 – Programación CUDA II PMPenGPU
Errores en tiempo de ejecución

A ver…
#include <cuda_runtime.h>

int main(int argc, char *argv[]){

float * inputGPU = NULL;


int size = 1000 * sizeof(float);
float * inputCPU = NULL;
inputCPU = (float*) malloc (size);

for (int j=0;j<1000);j++) {


inputCPU[j]=j+1.1f;
}

cudaMemcpy(inputGPU,inputCPU,size,cudaMemcpyHostToDevice);
CUDA_CHK( cudaFree(inputGPU) );
return 0;
}

Sigue sin fallar!!!! y entonces????


Clase 6 – Programación CUDA II PMPenGPU
Errores en tiempo de ejecución

• Los errores de CUDA son “asíncronos”.


– El fallo se produce en cudaMemcopy.
– La llamada a cudaFree se realiza pero sin ejecutarse.
• Sugerencias:
– Envolver todas las llamadas la biblioteca CUDA con CUDA_CHK()

Clase 6 – Programación CUDA II PMPenGPU


Errores en tiempo de ejecución
Ahora si!!!
#include <cuda_runtime.h>

int main(int argc, char *argv[]){

float * inputGPU = NULL;


int size = 1000 * sizeof(float);
float * inputCPU = NULL;
inputCPU = (float*) malloc (size);

for (int j=0;j<1000);j++) {


inputCPU[j]=j+1.1f;
}

CUDA_CHK(cudaMemcpy(inputGPU,inputCPU,size,cudaMemcpyHostToDevice));
CUDA_CHK(cudaFree(inputGPU));
return 0;
}

GPUassert: invalid argument [Link] 26 (línea del cudaMemcpy)


Clase 6 – Programación CUDA II PMPenGPU
Errores en tiempo de ejecución

• En las invocaciones a kernels:


– No se puede envolver la llamada al kernel con CUDA_CHK()
– Como los kernels se ejecutan de forma asíncrona, se debe realizar un
cudaDeviceSynchronize()luego de la invocación al kernel.
– La invocación a cudaDeviceSynchronize() puede ser envuelta
CUDA_CHK().

– La sincronización obliga a que el kernel llegue hasta el final de su


ejecución, por lo que nos devuelve los errores en la ejecución del kernel.

Clase 6 – Programación CUDA II PMPenGPU


Errores en tiempo de ejecución

• Veamos con un ejemplo, agreguemos kernel1:


__global__ void kernel1(float *v)
{
int i = threadIdx.x;

v[i*1000] = v[i]+v[i];
}

Clase 6 – Programación CUDA II PMPenGPU


Errores en tiempo de ejecución
#include <cuda_runtime.h>

int main(int argc, char *argv[]){

float * inputGPU = NULL;


int size = 1000 * sizeof(float);
float * inputCPU = NULL;
inputCPU = (float*) malloc (size);

for (int j=0;j<1000);j++) {


inputCPU[j]=j+1.1f;
}
kernel1<<<1,10000>>>(inputGPU);
CUDA_CHK (cudaDeviceSynchronize());
CUDA_CHK(cudaMemcpy(inputGPU,inputCPU,size,cudaMemcpyHostToDevice));
CUDA_CHK(cudaFree(inputGPU));
return 0;
}

GPUassert: invalid argument [Link] 36 (línea del


cudaDeviceSynchronize)
Clase 6 – Programación CUDA II PMPenGPU
Errores en tiempo de ejecución
#include <cuda_runtime.h>

int main(int argc, char *argv[]){

float * inputGPU = NULL;


int size = 1000 * sizeof(float);
CUDA_CHK (cudaMalloc((void **)&inputGPU, size));
float * inputCPU = NULL;
inputCPU = (float*) malloc (size);

for (int j=0;j<1000);j++) {


inputCPU[j]=j+1.1f;
}
CUDA_CHK(cudaMemcpy(inputGPU,inputCPU,size,cudaMemcpyHostToDevice));
kernel1<<<1,10000>>>(inputGPU);
CUDA_CHK (cudaDeviceSynchronize());
CUDA_CHK(cudaFree(inputGPU));
return 0;
}
NO DA ERROR!!!!! Cómo puede ser???

Clase 6 – Programación CUDA II PMPenGPU


Errores en tiempo de ejecución

• Hay dos errores:


– Uno en la invocación: no pueden haber 10000 hilos en un bloque.
– Otra en el propio kernel: que accede a memoria no reservada.
– El primer error hace que el kernel no se ejecute por lo que no
detectamos el error en el cudaDeviceSynchronize()pero tampoco
es capturado.
– Para capturar ese error debemos hacer una invocación a
cudaGetLastError() envuelta en CUDA_CHK() entre la llamada
al kernel y la sincronización.

Clase 6 – Programación CUDA II PMPenGPU


Errores en tiempo de ejecución
#include <cuda_runtime.h>

int main(int argc, char *argv[]){

float * inputGPU = NULL;


int size = 1000 * sizeof(float);
CUDA_CHK cudaMalloc((void **)&inputGPU, size));
float * inputCPU = NULL;
inputCPU = (float*) malloc (size);

for (int j=0;j<1000);j++) {


inputCPU[j]=j+1.1f;
}
CUDA_CHK(cudaMemcpy(inputGPU,inputCPU,size,cudaMemcpyHostToDevice));
kernel1<<<1,10000>>>(inputGPU);
CUDA_CHK (cudaGetLastError() );
CUDA_CHK (cudaDeviceSynchronize());
CUDA_CHK(cudaFree(inputGPU));
return 0;
}

GPUassert: invalid configuration argument [Link] 38 (línea del


cudaGetLastError()
Clase 6 – Programación CUDA II PMPenGPU
Errores en tiempo de ejecución
#include <cuda_runtime.h>

int main(int argc, char *argv[]){

float * inputGPU = NULL;


int size = 1000 * sizeof(float);
CUDA_CHK cudaMalloc((void **)&inputGPU, size));
float * inputCPU = NULL;
inputCPU = (float*) malloc (size);

for (int j=0;j<1000);j++) {


inputCPU[j]=j+1.1f;
}
CUDA_CHK(cudaMemcpy(inputGPU,inputCPU,size,cudaMemcpyHostToDevice));
kernel1<<<1,1000>>>(inputGPU);
CUDA_CHK (cudaGetLastError() );
CUDA_CHK (cudaDeviceSynchronize());
CUDA_CHK(cudaFree(inputGPU));
return 0;
}

GPUassert: an illegal memory access was encountered [Link] 39


(línea del cudaDeviceSynchronize())
Clase 6 – Programación CUDA II PMPenGPU
Errores en tiempo de ejecución

• En resumen:
– Envolver todas las llamadas la biblioteca CUDA con CUDA_CHK()
– Incluir un CUDA_CHK(cudaGetLastError()) inmediatamente
después de la invocación al kernel.
– Incluir un CUDA_CHK(cudaDeviceSynchronize())
inmediatamente después del paso anterior.

Clase 6 – Programación CUDA II PMPenGPU


Código PTX

Clase 6 – Programación CUDA II PMPenGPU


Código PTX

Recordemos la C/C++ CUDA


Application
compilación

NVCC CPU Code


El código PTX puede
obtenerse
PTX Code

Puede dar pistas de


PTX to Target
posibles optimizaciones Compiler
del código

G80 … GPU

Target code
Clase 6 – Programación CUDA II PMPenGPU
Código PTX

• El código PTX se puede obtener compilando con la flag –ptx:

nvcc –ptx [Link]

• Se genera el archivo [Link]


• El código PTX permite hilar muy fino en aspectos del código
CUDA que impactan en la performance.

Clase 6 – Programación CUDA II PMPenGPU


Código PTX

__global__ void CopiaRara(float * output, float * input){


int idx = blockIdx.x * blockDim.x + threadIdx.x;
output[idx] = input[idx] * 2.27;
}

• Inspeccionemos el código resultante (ver [Link]).

[Link].f32 %f1, [%rd4+0]; // id:17


cvt.f64.f32 %fd1, %f1; //
mov.f64 %fd2, 0d400228f5c28f5c29; // 2.27
mul.f64 %fd3, %fd1, %fd2; //
[Link].f32.f64 %f2, %fd3; //

Clase 6 – Programación CUDA II PMPenGPU


Código PTX

__global__ void CopiaRara(float * output, float * input){


int idx = blockIdx.x * blockDim.x + threadIdx.x;
output[idx] = input[idx] * 2.27f;
}

• Inspeccionemos el código resultante (ver [Link]).


• El mismo fragmento de programa se transformó en:

[Link].f32 %f1, [%rd4+0]; // id:17


mov.f32 %f2, 0f401147ae; // 2.27
mul.f32 %f3, %f1, %f2;

Clase 6 – Programación CUDA II PMPenGPU


Algunas recomendaciones adicionales
sobre performance

Clase 6 – Programación CUDA II PMPenGPU


Algunas recomendaciones adicionales
sobre performance

• Evitar la divergencia de hilos dentro de un warp.


• El número de bloques debe ser mayor al número de
multiprocesadores:
- De forma de mantener a todos los multiprocesadores ocupados.
- Para permitir ocultar latencias cuando un bloque está trancado con un
__syncthreads() debe ser mayor al doble del número de
multiprocesadores.
• El número de hilos por bloque debe ser un múltiplo de 32
(tamaño de warp).

Clase 6 – Programación CUDA II PMPenGPU

También podría gustarte