English · Español
00 — Por qué cuantización (y por qué no es compresión)¶
La cuantización (quantization) no es comprimir un fichero; es cambiar la posición de tu kernel en el roofline. Mismo cómputo, menos bytes, intensidad aritmética más alta. Por eso es rápido — no por "tener menos información".
El planteamiento ingenuo (y por qué despista)¶
Un lector primerizo oye "la cuantización a INT8 reduce el modelo 4×" y concluye: la cuantización es un esquema de compresión. ZIP hace los ficheros más pequeños; INT8 hace los pesos más pequeños; misma idea, ¿no?
Incorrecto, y el matiz importa.
ZIP ahorra bytes en disco. Cuando descomprimes y cargas en RAM, pagas el mismo coste de memoria que antes. ZIP es una optimización de almacenamiento; no acelera tu programa una vez en ejecución.
La cuantización ahorra bytes en vuelo — durante cada forward pass, en cada carga de peso desde DRAM a cache, en cada llenado de registro. El modelo es más pequeño en disco y en cache y en tránsito, cada vez que se ejecuta. Es un tipo de ganancia fundamentalmente distinto.
Para ver por qué importa, recordemos el roofline de la Fase 1.
El re-planteamiento por roofline¶
Un único bloque transformer durante la inferencia pasa la mayor parte de su tiempo en dos operaciones:
Linear(x)— un producto matriz-vectory = W x + b, dondeW ∈ ℝ^(out×in).Attention(q, k, v)— ver la Fase 27 para la historia a nivel de kernel; en esta fase, trátalo como una secuencia de productos de matrices más un softmax.
Para batch de tamaño 1 (el caso realista para inferencia local en el i5-8250U de Borja), Linear(x) es un producto matriz-vector, no un producto matriz-matriz. La intensidad aritmética es brutal:
- FLOPs:
2 × out × in(una multiplicación + una acumulación por peso). - Bytes cargados:
4 × out × in(cada peso, FP32, una vez) +4 × in(el vector de activación, FP32). Los pesos dominan. - Intensidad:
I = 2 out·in / (4 out·in) ≈ 0.5 FLOPs/byte.
Entre un cuarto y medio FLOP por byte. De la derivación del roofline de la Fase 1, el punto de equilibrio máquina del i5-8250U es I_crit ≈ 10 FLOPs/byte. La inferencia en MiniGPT está limitada por ancho de banda por un factor de ~20×.
Ahora cuantiza los pesos a INT8. La matemática no cambia — los FLOPs siguen siendo 2 × out × in. Pero los bytes cargados bajan:
- Bytes:
1 × out·in(pesos INT8) +4 × in(activación FP32). Los pesos aún dominan. - Intensidad:
I = 2 out·in / out·in = 2 FLOPs/byte. 4× más alta.
INT4 por-grupo: los pesos empaquetan 2 por byte (con una pequeña sobrecarga para escalas). Intensidad: I ≈ 4 FLOPs/byte. 8× más alta.
En un kernel limitado por ancho de banda, intensidad es velocidad. INT8 eleva el punto sobre la línea del techo de memoria por 4×; INT4 por 8×. Al roofline no le importa que la precisión numérica sea menor — la FPU simplemente ejecuta multiply sobre cualesquiera bits que lleguen al registro.
El presupuesto de error¶
Ese es el lado de la ganancia. El lado del coste es el error de cuantización (quantization error).
Un peso w almacenado en FP32 tiene aproximadamente 7 dígitos decimales de precisión. Almacenado en INT8 con escala (scale) s, tiene 256 valores representables en total. El error de redondeo es como mucho s/2 por peso. A lo largo de out × in pesos, los errores se acumulan, luego se propagan por las capas, luego por el softmax, luego hasta la pérdida.
La pregunta estándar — y la que esta fase responde empíricamente — es: ¿cuán grande llega a ser ese error y dónde se rompe el modelo?
Las respuestas, esbozadas por anticipado:
- INT8 por-tensor: el error es lo bastante pequeño para que la perplejidad apenas se mueva (< 1%) en capas bien comportadas, salvo que haya un outlier en la distribución de activación. Los outliers hinchan la escala (scale), comiéndose la precisión del caso típico. Este es el modo de fallo que LLM.int8() resuelve.
- INT8 por-canal: los errores están acotados por fila, así que una sola fila ruidosa no envenena todo el tensor. El gap de PPL suele ser < 2%.
- INT4 por-grupo: 16 valores por "canal" en lugar de 256, particionados en grupos de 64 o 128 dentro de un canal. Gap de PPL del 5–15% según la calibración.
- INT4 con GPTQ: en lugar de redondeo al más cercano, GPTQ usa la Hessiana de la distribución de activación para redistribuir el error de redondeo a columnas que aún no hemos cuantizado. El gap de PPL cae al 1–3% — a costa de unos pocos minutos de calibración por una sola vez.
En la Fase 26 implementamos INT8 (por-tensor y por-canal) y una variante de GPTQ sobre una sola capa lineal. Los demás esquemas se leen pero no se programan.
Por qué esto viene después de la Fase 24 (introducción a PyTorch)¶
La cuantización necesita infraestructura de nivel framework: hooks para registrar activaciones durante la calibración, ops de fake-quant que simulan INT8 en FP32 (para no tener que escribir un kernel INT8 real), módulos cuyo forward alterna entre rutas cuantizada y FP32. Podríamos hacerlo en NumPy — pero cada capa del modelo necesitaría recableado y el código boilerplate eclipsaría la matemática.
PyTorch se introdujo en la Fase 24 precisamente para que la Fase 26 pueda apoyarse en las primitivas de torch.quantization (o escribir las nuestras equivalentes — ver el BLUEPRINT). Usamos PyTorch como sustrato para la matemática, no como un cuantizador de caja negra: cada paso de cuantización está programado a mano; PyTorch solo gestiona el almacenamiento del tensor y el grafo de autograd (que no usamos aquí porque esto es post-entrenamiento).
Por qué esto viene antes de la Fase 27 (atención moderna)¶
La Fase 27 introduce FlashAttention y PagedAttention. Ambas están dominadas por el mismo insight: el tráfico de memoria es el cuello de botella. FlashAttention mantiene el working set en SRAM; PagedAttention pagina la KV cache. Ambas son manipulaciones del roofline.
La Fase 26 entrena el ojo del lector para ver "este kernel mueve B bytes; recorta B y subirás la intensidad". La Fase 27 aplica el mismo ojo a la atención. La Fase 28 (LoRA) lo aplica al ajuste fino (fine-tuning). La Fase 29 (RAG) lo aplica a la inferencia a escala.
La cuantización es la demostración más limpia del principio porque los FLOPs son literalmente idénticos antes y después — solo cambian los bytes. Al final de esta fase, cuando Borja oiga "FlashAttention es 3× más rápido", la siguiente frase en su cabeza debería ya ser "...porque cambió el conteo de bytes, no el conteo de FLOPs".
Qué medimos en la máquina de Borja¶
La DoD incluye una re-representación del roofline de la Fase 1 con tres puntos de inferencia de MiniGPT superpuestos: FP32, INT8 por-canal, INT4 por-grupo. La imagen esperada:
- Punto FP32: baja intensidad, bajo rendimiento — muy por debajo del techo de memoria porque los outliers de activación y el overhead de Python dejan ancho de banda sin usar.
- Punto INT8: 4× más intensidad, 3–4× más tokens/seg.
- Punto INT4: 8× más intensidad, 5–6× más tokens/seg (menos de 8× porque las escalas por-grupo añaden sobrecarga por fetch y el unpacking no es gratis en AVX2 sin VNNI).
Si los ratios medidos divergen de estas predicciones, el laboratorio incluye preguntas de seguimiento sobre el porqué — ancho de banda no saturado de verdad, ruta INT8 de AVX2 inexistente, ruta dequantize-en-FP32 activa, etc.
Resumen en un párrafo¶
La cuantización es una optimización de roofline, no una optimización de compresión. Eleva la intensidad aritmética de kernels limitados por ancho de banda (producto matriz-vector, atención, etc.) reduciendo los bytes por peso sin cambiar los FLOPs. El coste es un error de redondeo acotado, que controlamos eligiendo la unidad de escala (por-tensor → por-canal → por-grupo) y el algoritmo de cuantización (round-to-nearest → GPTQ). La ganancia es real porque la mayoría de la inferencia local está limitada por ancho de banda por un factor de 10–20×; INT8 cierra la mitad del gap, INT4 cierra la mayor parte. El resto de la Fase 26 deriva las fórmulas, acota los errores y mide los compromisos en el MiniGPT de Borja.
Siguiente: theory/01-number-formats.md.