¿Te resulta más fácil leer con fondo crema, un tono de color, más contraste o un texto más grande? Elige tus colores y tu texto una sola vez, para todas las páginas.

Lección gratuita

Memoria y cuellos de botella en una GPU

Después de esta lección: Puedo decidir si un trabajo está limitado por el cálculo o por la memoria.

Lección de 60 min2 comandosLaboratorio de computación paralela: cómo piensa una GPU, sin GPU, en JupyterLab

Por qué una GPU potente no acelera

Un cuello de botella es la parte de un sistema que limita la velocidad de todo lo demás. En una GPU suele ser la memoria: mover los datos cuesta más que sumarlos.

Saber si un trabajo está limitado por el cálculo o por la memoria te dice si comprar más potencia servirá de algo. En este laboratorio no hay GPU: todo son cuentas sobre un dispositivo inventado, sin medir tiempos.

Al final de esta lección

Nota

Puedo decidir si un trabajo está limitado por el cálculo o por la memoria.

Palabras clave

PalabraQué significa
Ancho de bandalos bytes que la memoria mueve por segundo
Ritmo de cálculolas operaciones que el dispositivo hace por segundo
Cuello de botellael límite que decide cuánto tarda todo
Transferenciamover datos entre dos memorias, como la del ordenador y la de la GPU
Acceso agrupadohilos vecinos que leen direcciones vecinas

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Un dispositivo tiene dos límites

El primero es cuántos bytes mueve la memoria por segundo: el ancho de banda. El segundo es cuántas operaciones hace por segundo: el ritmo de cálculo.

Un trabajo necesita mover cierta cantidad de bytes y hacer cierto número de sumas. Tarda lo que pida cada límite, y manda el mayor de los dos.

Dos divisiones dicen qué límite manda

El tiempo de memoria es los bytes movidos entre el ancho de banda. El tiempo de cálculo es las sumas entre el ritmo.

Si el de memoria es mayor, el trabajo está limitado por la memoria. Si no, por el cálculo. Son cuentas con números que se dan, no tiempos medidos.

Ejemplo: 200 GB/s y 4 teraflops

TrabajoBytes movidosSumasMemoriaCálculoLimita
Suma de vectores de 1 millón12.000.0001.000.00060 µs0,25 µsLa memoria
Producto de matrices de 5003.000.000250.000.00015 µs62,5 µsEl cálculo

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Calcular qué límite manda

Código: python
# Un dispositivo inventado: mueve 200 GB por segundo y hace 4 billones de sumas por segundo.
ancho_banda = 200 * 10**9
ritmo = 4 * 10**12
bytes_por_numero = 4


def informe(nombre, bytes_movidos, sumas):
    memoria = bytes_movidos / ancho_banda * 10**6
    calculo = sumas / ritmo * 10**6
    print(nombre)
    print("  microsegundos de memoria:", round(memoria, 2))
    print("  microsegundos de cálculo:", round(calculo, 2))
    if memoria > calculo:
        print("  limita: la memoria")
    else:
        print("  limita: el cálculo")


n = 1000000
informe("Suma de vectores de 1 millón", 3 * n * bytes_por_numero, n)
m = 500
informe("Producto de matrices de 500", 3 * m * m * bytes_por_numero, 2 * m * m * m)
# Cambia 200 por 800 en ancho_banda y vuelve a ejecutar: ¿qué trabajo cambia?

Mover los datos a la GPU también cuesta

Los datos viven primero en la memoria del ordenador y hay que copiarlos a la de la GPU, y el resultado vuelve. Esa transferencia pasa por un enlace más lento que la memoria de la GPU.

Con un enlace de 25 GB/s, copiar 12 millones de bytes cuesta 480 µs, mucho más que las sumas de la tabla. Para un trabajo pequeño, copiar puede no compensar.

Hilos vecinos deben leer direcciones vecinas

La memoria entrega los datos en líneas, por ejemplo de 16 bytes. Si cuatro hilos vecinos leen números seguidos, una sola línea les sirve a todos. Eso es un acceso agrupado.

Si cada hilo lee un número lejano, hay que traer cuatro líneas para usar cuatro números. Sobra tráfico y el trabajo va más lento.

Contar las líneas que se traen

Código: python
# Cuatro hilos leen un número de 4 bytes cada uno. La memoria se trae en líneas de 16 bytes.
hilos = 4
tamano = 4
linea = 16
for paso in [1, 4]:
    direcciones = []
    for hilo in range(hilos):
        direcciones.append(hilo * paso * tamano)
    lineas = []
    for direccion in direcciones:
        numero = direccion // linea
        if numero not in lineas:
            lineas.append(numero)
    print("paso", paso, "direcciones", direcciones, "líneas traídas", len(lineas))
# Con paso 1 los cuatro leen una sola línea. Con paso 4, cuatro.

Qué imprimen los dos scripts del laboratorio

El laboratorio trae steps/transfer.py y steps/coalesce.py. El primero hace estas mismas cuentas con otro dispositivo, de 500 GB/s y 10 teraflops. El segundo cuenta líneas de memoria para ocho hilos.

Las palabras que imprimen están en inglés, como limited by y lines fetched. No miden tiempos y siempre imprimen lo mismo.

Ver qué trabajo limita a qué

Comando
docker exec jupyter python3 steps/transfer.py

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Ver cuántas líneas toca cada reparto

Comando
docker exec jupyter python3 steps/coalesce.py

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Un error muy común

Advertencia

Mucha gente cree que una GPU más rápida acelera cualquier trabajo. Solo acelera los limitados por el cálculo. Si la memoria manda, hace falta mover menos bytes, agrupar los accesos o hacer más cuentas con cada byte que se trae.

Sobre este laboratorio y su código: Laboratorio de computación paralela: cómo piensa una GPU, sin GPU, en JupyterLab

¿Enseñas a una clase? Asigna este módulo a tus alumnos con guía del profesor, tareas con corrección automática y un laboratorio para cada alumno.