Skip to content

English · Español

02 — Jerarquía de memoria: caches, DRAM, NUMA, PCIe, SSD

La jerarquía de memoria es la columna vertebral de toda intuición de rendimiento en IA (AI). Cada nivel tiene una latencia (cuán rápido empieza) y un ancho de banda (cuán rápido sostiene). Memoriza los órdenes de magnitud — el resto de la carrera son corolarios.


Los números que debes memorizar

Esta es la tabla de la Fase 1. Si interiorizas una sola cosa de las primeras cien páginas del currículo, que sea esta. Todos los números son del orden de magnitud para una CPU de portátil de la era 2018 (cercana al i5-8250U de Borja).

Nivel Capacidad Latencia (ciclos) Latencia (ns) Ancho de banda
Registro ~16 × 64-bit 0 0
Cache L1 32 KiB / core 4 ~1 ~1 TB/s
Cache L2 256 KiB / core 12 ~4 ~500 GB/s
Cache L3 6 MiB compartida 40 ~12 ~200 GB/s
DRAM (RAM principal) 16–64 GiB ~200 ~70 ~20 GB/s
NVMe SSD 256 GiB–4 TiB ~30.000 ~10.000 (10 µs) ~3 GB/s lectura
Red (gigabit) ~300.000 ~100.000 (100 µs) ~125 MB/s

Las latencias abarcan cinco órdenes de magnitud entre L1 y SSD. Los anchos de banda abarcan tres órdenes de magnitud. Cada argumento de rendimiento en hardware de IA funciona debido a esta brecha.

Drill: antes de seguir leyendo, cierra este archivo. Reconstruye la tabla de memoria. Acércate a menos del 2× en cada número. Si no puedes, no conoces la tabla — y no serás capaz de hacer un argumento de roofline sin ella.

Por qué existen las caches (re-derivación, ya que la olvidaste)

La RAM principal está a 70 ns. Una CPU a 3 GHz va a 333 ps. Así que una sola lectura de RAM sin cache cuesta ~210 ciclos de CPU. En esos 210 ciclos, un core con AVX2 podría haber hecho ~3400 multiplicaciones fp32 (4 cores × 8 wide × 2 FMA = 64 fp32/ciclo × 210 ciclos ÷ 4 porque 1 core se detiene en la carga).

La CPU pasa 210× más tiempo esperando que computando si cada acceso golpea la DRAM.

Las caches cierran la brecha manteniendo los datos usados recientemente más cerca de la ALU. El principio es la localidad:

  1. Localidad temporal. Si lees la dirección A, probablemente leerás A de nuevo pronto. (La mayoría del código reutiliza variables.)
  2. Localidad espacial. Si lees la dirección A, probablemente leerás A+1, A+2, … pronto. (La mayoría del código recorre arrays / structs secuencialmente.)

Así que las caches almacenan datos en líneas (típicamente 64 bytes), no bytes individuales. Cargar un valor fp32 carga sus 15 vecinos gratis. Por eso el acceso a array con stride 1 es rápido y el acceso con stride 1024 es lento — estás pagando por 15 fp32 que no usas.

El Lab 02 hace esto visible.

Cache hit, cache miss, cache line y el "working set"

Un cache hit significa que los datos estaban en cache; el coste de acceso es la latencia de la cache (1–12 ns). Un cache miss significa que los datos no estaban allí; el coste de acceso es la latencia del siguiente nivel, más el tiempo para desalojar una línea vieja.

Una cache line es la unidad de transferencia. 64 bytes en x86. Cuando cargas un byte, la CPU trae el trozo alineado de 64 bytes completo que lo contiene. Por eso el false sharing (dos hilos escribiendo a variables distintas en la misma cache line) es catastrófico — la línea rebota entre las L1 de los cores, invalidando cada uno la copia del otro.

El working set de un kernel son los datos únicos que toca en un bucle ajustado. Si el working set cabe en L1 (32 KiB), el kernel corre a velocidad L1. Si solo cabe en L2, corre a velocidad L2. Si desborda L3, corre a velocidad DRAM.

Esta es la razón completa por la que el matmul por bloques / tileado bate al matmul ingenuo por 50×: el matmul por bloques mantiene cada tile en L1, por lo que cada fp32 se lee desde DRAM una vez por tile, no una vez por iteración del bucle interno. La Fase 3 deriva el tamaño de bloque a partir de este razonamiento exacto.

Asociatividad (brevemente, ya que lscpu la muestra)

Las caches no pueden almacenar cualquier línea en cualquier slot — eso requeriría comprobar cada slot en cada acceso. En su lugar, cada dirección de línea mapea a un pequeño conjunto de slots (un set). Una cache "8-way set-associative" significa que cada línea tiene 8 slots candidatos; la CPU comprueba los 8 en paralelo.

Mayor asociatividad = menos conflict misses pero más área de chip. L1 suele ser 8-way. L2 es 4–16-way. L3 es 16-way o más.

No vas a optimizar para la asociatividad en la Fase 1. La mencionamos porque lscpu la imprime y deberías saber lo que significa.

DRAM, no "RAM"

La etiqueta de consumidor "RAM" es internamente DRAM — Dynamic Random Access Memory. Cada bit es un condensador diminuto que pierde carga y debe ser refrescado miles de veces por segundo. Por eso la DRAM es lenta: cada acceso comienza encontrando la fila correcta en el banco correcto, abriéndola (~30 ns), y solo entonces leyendo.

La DRAM tiene bancos (grupos de filas que se pueden acceder en paralelo) y canales (buses paralelos hacia la CPU). El i5-8250U tiene 2 canales × 1 DIMM × 64 bancos/DIMM ≈ 128 bancos en vuelo. El ancho de banda pico de 20 GB/s asume que dispersas tus accesos entre ellos. Peor caso (un banco, aciertos de fila aleatorios): ~2 GB/s.

Implicación: un kernel que toca DRAM aleatoriamente obtiene ~10× menos ancho de banda que uno que la toca secuencialmente. El acceso secuencial es tu amigo.

NUMA (mencionado por vocabulario)

Un sistema NUMA (Non-Uniform Memory Access) tiene múltiples sockets de CPU, cada uno con sus propios controladores de DRAM. Acceder a memoria en tu propio socket es rápido; acceder a memoria en el otro socket pasa por el interconnect entre sockets (UPI en Intel) y es 2–3× más lento.

El portátil de Borja es single-socket — sin NUMA en la práctica. La Fase 1 menciona NUMA porque: - Cada paper de GPU de datacenter habla de ello. - Pinear procesos a un nodo NUMA (numactl --cpunodebind=0 --membind=0 ./script) es un truco común de la Fase 35 (distribuido).

Para los labs de la Fase 1, sin experimento NUMA. Solo conoce la palabra.

PCIe (camino a la iGPU y a NVMe)

PCIe es el bus que conecta la CPU a los periféricos — GPUs discretas, SSDs NVMe, tarjetas de red. Las generaciones importan:

Gen Ancho de banda por lane Enlace típico a una GPU
3.0 1 GB/s 16 lanes ≈ 16 GB/s
4.0 2 GB/s 16 lanes ≈ 32 GB/s
5.0 4 GB/s 16 lanes ≈ 64 GB/s

Para la iGPU de Borja (Intel UHD 620), no hay enlace PCIe a la GPU — la iGPU está en la misma die que la CPU y comparte la misma DRAM. Esto es inusual y vale la pena tenerlo en cuenta para la Fase 23, cuando "memoria de GPU" de repente signifique "un pool separado, más rápido, más pequeño a través de PCIe".

Los SSDs NVMe normalmente usan 4 lanes PCIe — 4 lanes × 1 GB/s (Gen 3) = 4 GB/s teóricos, ~3 GB/s prácticos.

Las GPUs NVIDIA de datacenter están enlazadas entre sí mediante NVLink — un interconnect propietario mucho más rápido que PCIe (~600-900 GB/s agregados en H100). El entrenamiento multi-GPU (Fase 35) explota NVLink intensivamente. La Fase 1 lo menciona para que la palabra no aparezca sin anunciar en la Fase 24.

Latencia vs throughput — un ejemplo trabajado

Una confusión común: "Tengo 20 GB/s de ancho de banda, así que 70 ns de latencia no importan".

Importa si tu patrón de acceso no permite paralelismo. El ancho de banda asume que tienes muchas peticiones pendientes a la vez (pipelined). La latencia domina si las peticiones son serie — cada una espera a que la anterior termine.

En concreto: - memcpy secuencial de un buffer de 1 GB: gana el ancho de banda. ~50 ms a 20 GB/s. - Recorrido de lista enlazada de 10M nodos (cada carga depende de la anterior): gana la latencia. 10M × 70 ns = 700 ms. 14× más lento para el mismo recuento de bytes.

Esto es la ley de Little aplicada a memoria: throughput = paralelismo / latencia. Si no puedes exponer paralelismo (sin prefetching, sin emisión fuera de orden), el throughput colapsa a 1 / latencia accesos por segundo.

El Lab 02 hace esto concreto midiendo tanto un recorrido secuencial como un recorrido aleatorio sobre el mismo buffer.

Implicaciones para la IA

Unas pocas aplicaciones más de la tabla, construyendo sobre theory/00-motivation.md:

  • Las matrices de pesos que caben en L3 (~6 MiB = 1.5M fp32) corren rápido. Las matrices de pesos que no caben en L3 están DRAM-bound. Un embedding pequeño de Llama (vocab 32k × dim 4k = 128M fp32) es mucho mayor que L3 → DRAM-bound durante la pasada forward.
  • El KV cache de attention (Fase 22) se hace grande rápido para secuencias largas. Una vez que KV no cabe en cache, la attention de cada token está DRAM-bound — por eso la latencia de inferencia de contexto largo escala con la longitud de secuencia aunque los FLOPS solo crezcan linealmente.
  • Las activaciones durante el entrenamiento son mayores que los pesos. La razón por la que existe el gradient checkpointing (Fase 18) es que las activaciones se derraman a DRAM y dominan el tráfico de memoria.

Cada una de estas es un argumento de roofline. La gráfica del roofline es el siguiente archivo.

Recapitulación en un párrafo

La jerarquía de memoria es una escalera: L1 / L2 / L3 / DRAM / SSD / red, con cada peldaño 5–10× más lento y ~10× mayor que el anterior. Las caches cierran la brecha L1↔DRAM explotando localidad temporal y espacial a granularidad de líneas de 64 bytes. El acceso secuencial obtiene ancho de banda; el acceso aleatorio obtiene latencia. Cada optimización de rendimiento de IA o bien mantiene el working set en un nivel de cache más alto u oculta la latencia con paralelismo. El modelo roofline (siguiente) es el cuadro unificado.


Siguiente: theory/03-roofline-model.md.