English · Español
02 — Escalas y ceros: el mapa de cuantización¶
La cuantización (quantization) es una función afín entre reales y enteros:
q = round(x/s) + z. Aquí derivamos el mapa simétrico (z=0) y asimétrico, acotamos el error máximo y mostramos por qué la granularidad des(por-tensor, por-canal, por-grupo) importa más que el número de bits.
El mapa¶
La función de cuantización mapea un valor real x ∈ ℝ a un entero q ∈ ℤ usando dos parámetros:
- Escala (scale)
s > 0— el tamaño de un paso de cuantización en unidades reales. - Cero (zero-point)
z ∈ ℤ— el entero que representa el cero real.
Hacia adelante (quantize):
Hacia atrás (dequantize):
El conjunto de valores representables es {s(q - z) : q ∈ [q_min, q_max]}. Para INT8, [q_min, q_max] = [-128, 127] (con signo) o [0, 255] (sin signo).
Simétrico vs asimétrico¶
La cuantización simétrica fija z = 0. El conjunto de valores representables está centrado en el cero real, espaciado uniformemente por s. Eligiendo s tal que s × 127 = M donde M = max(|x|):
Desperdiciamos la bandeja en q = -128 por simetría. Este off-by-one es convencional y evita el caso borde asimétrico donde q_min no tiene un compañero simétrico.
La cuantización asimétrica elige z de forma que el rango [x_min, x_max] mapea a [0, 255] (INT8 sin signo):
La asimétrica es mejor cuando la distribución es unilateral (p. ej., activaciones post-ReLU: todas ≥ 0). La simétrica es mejor cuando la distribución es bilateral y centrada en cero (p. ej., pesos de un Linear tras inicialización estándar).
Para la Fase 26 usamos simétrica para pesos, asimétrica para activaciones. Esto coincide con GPTQ, LLM.int8() y la mayoría de los esquemas de producción.
Cotas de error¶
El error de cuantización por elemento es e = x - \hat{x}. Queremos \sup_x |e|.
Para INT8 simétrico con escala (scale) s = M/127:
La función round comete como mucho s/2 de error por elemento:
Esta es la cota por-elemento. Sobre N elementos con segundo momento acotado, el error L2 escala como \sqrt{N} \cdot s/\sqrt{12} (asumiendo que los errores de redondeo son independientes y uniformemente distribuidos en [-s/2, s/2], lo cual es aproximadamente cierto para distribuciones no patológicas). El factor 1/\sqrt{12} viene de la varianza de una distribución uniforme en [-s/2, s/2]:
Dónde es ajustada esta cota¶
Cuando la distribución es uniforme en [-M, M], los errores de redondeo realmente son uniformes en [-s/2, s/2], el segundo momento es exactamente s^2/12 y la cota es estrecha.
Dónde es laxa esta cota (y qué hacer)¶
Cuando la distribución tiene outliers — una fracción ínfima de elementos con |x| ≫ \sigma_x — esos outliers fuerzan que M (y por tanto s) sea enorme. La mayoría de los elementos entonces viven cerca de cero, muy por debajo de la resolución de la rejilla de cuantización: los bits efectivos usados por elemento colapsan de 8 a ~2-3.
Este es el hecho práctico más importante sobre cuantización. La inflación de escala por outliers explica por qué:
- INT8 por-tensor falla en las proyecciones de salida de atención (una fila de la matriz tiene 100× la magnitud de las demás).
- LLM.int8() existe en absoluto (factorizar las filas outlier a FP16; INT8 el resto).
- SmoothQuant funciona (migrar la magnitud outlier de las activaciones a los pesos, que pueden absorberla).
La unidad de s: por-tensor vs por-canal vs por-grupo¶
La "unidad" es la losa de pesos que comparten una sola escala (scale).
| Granularidad | Una s por |
Sobrecarga | Calidad | Cuándo |
|---|---|---|---|---|
| Por-tensor | Matriz de pesos W entera |
1 escalar por capa | Peor | Solo baseline |
| Por-canal | Cada fila de salida de W |
out escalares por capa |
Media | Por defecto en INT8 |
| Por-grupo | Cada bloque contiguo de g pesos dentro de una fila (p. ej., g=64) |
out × in/g escalares por capa |
Mejor | Por defecto en INT4 |
Para un Linear(in=768, out=768):
- Por-tensor: 1 escalar. Almacenado en FP16 = 2 bytes de sobrecarga.
- Por-canal: 768 escalares. 1.5 KiB de sobrecarga.
- Por-grupo (g=64):
768 × (768/64) = 768 × 12 = 9216escalares. 18 KiB de sobrecarga.
En términos de INT4, la matriz de pesos en sí es 768 × 768 / 2 = 288 KiB. La sobrecarga por-grupo (18 KiB) añade un 6%, bajando los bits efectivos por peso de 4 a ~4.3. Vale la pena: INT4 por-grupo alcanza perplejidades que INT8 por-tensor no.
Por qué la granularidad fina ayuda tanto¶
Considera una fila de W con dos grupos naturales: 99% de los pesos en [-1, 1], 1% en [-100, 100]. Escala por-fila s = 100/127 ≈ 0.8, así que el grupo del 99% se cuantiza a resolución 0.8 — cada peso en [-0.4, 0.4] colapsa a 0. Hemos destruido efectivamente la mayor parte de la información.
La escala (scale) por-grupo (tamaño de grupo 64) permite que cada grupo elija su propia escala. Dentro del grupo del 99%, los grupos ven M ≈ 1, escala s ≈ 0.008 — resolución 100× más fina. Los grupos que solo contienen outliers siguen recibiendo la mala escala, pero son un 1% de las filas.
Por esto INT4 por-grupo a menudo bate a INT8 por-tensor en perplejidad a pesar de tener la mitad de bits por peso.
Eligiendo M: máximo, percentil, MSE¶
El ingenuo M = max(|x|) es sensible a outliers. Tres alternativas:
- Clipping por percentil.
M = quantile(|x|, 0.999). Cualquier cosa por encima se recorta aM. Cambia un error pequeño de clipping por un gran error de inflación de escala. El laboratorio 00 barre el percentil y mira la curva de perplejidad. - Minimización del MSE. Elegir
Mpara minimizarE[(x - \hat{x})^2]sobre la distribución de calibración. Forma cerrada para distribuciones simétricas; numérica para las generales. - Divergencia KL. Usada por TensorRT. Elegir
Mpara minimizar el KL entre el histograma dexy el de\hat{x}.
Para la Fase 26 usamos (1) en el percentil 99.9 por defecto y (2) como comprobación de cordura. (3) es solo lectura.
Cuantizando activaciones¶
Los pesos son estáticos — podemos calcular M una vez en la calibración. Las activaciones son dinámicas — dependen de la entrada.
Dos estrategias:
Cuantización estática de activaciones. Pasar un conjunto de calibración (típicamente 128 muestras) por el modelo en FP32; registrar estadísticas de activación por capa; elegir s, z una vez; usarlas en inferencia. Rápido en inferencia; sensible a desplazamientos de distribución entre calibración y despliegue.
Cuantización dinámica de activaciones. Calcular M = max(|x|) al vuelo por cada entrada. Más precisa por muestra, pero el propio cálculo del max añade latencia, y en CPU la ruta condicional rompe la vectorización.
Para el PTQ de la Fase 26 usamos cuantización estática de activaciones para INT8 (coincide con LLM.int8() y la mayoría de los runtimes de CPU).
El forward de Linear en INT8¶
La ruta forward cuantizada para y = W x + b:
W_int8, s_W = quantize_symmetric_per_channel(W) # at calibration
s_x, z_x = ... # at calibration (static)
x_int8 = quantize_asymmetric(x, s_x, z_x) # at inference
y_int32 = W_int8 @ x_int8 # INT32 accumulator
y_float = s_W * s_x * (y_int32 - z_x * sum_over_in(W_int8))
y_float += b # bias in FP16/FP32
El acumulador INT32 es crítico: INT8 × INT8 puede hacer overflow de INT8 en unos pocos términos. El acumulador debe ser más ancho que las entradas. En CPUs con AVX-VNNI (Ice Lake+), hay una instrucción fusionada vpdpbusd que hace INT8 × INT8 → acumular INT32. El Kaby Lake R de Borja carece de VNNI; la ruta INT8 de PyTorch en esta CPU cae a una secuencia dequant-luego-fp32-matmul, que es más lenta que un matmul FP32 plano.
Esto importa para el Lab 02. No esperes que la inferencia INT8 sea más rápida en la máquina de Borja hasta que cross-compilemos o usemos los kernels INT8 AVX2 ajustados a mano de llama.cpp. El laboratorio mide bytes-en-disco y PPL; las medidas de velocidad se encuadran como "lo que veríamos en una CPU con VNNI".
Problemas de práctica¶
Soluciones en solutions/02-scales-and-zeros-ref.md (en la apertura de fase). Intenta sin ejecutar.
- Un cuantizador INT8 simétrico sobre un tensor con
M = 10. ¿Cuál ess? ¿Cuál es el error máximo por-elemento? Expresa el SQNR (signal-to-quantization-noise ratio en dB) asumiendo distribución uniforme en[-10, 10]. - Un
Linear(in=768, out=768). Calcula el almacenamiento en bytes para: (a) FP32, (b) INT8 por-tensor, © INT8 por-canal (escalas FP16), (d) INT4 por-grupo=64 (escalas FP16). Verifica la afirmación "INT4 por-grupo es ~4.3 bits efectivos". - Una fila de
Wtiene[w_1, ..., w_{63}, w_{64}] = [0.01, ..., 0.01, 50]. Cuantiza esta fila con INT8 por-canal y muestra qué le pasa aw_1, ..., w_{63}tras la dequantización. Ahora por-grupo con tamaño de grupo 32: misma pregunta. Explica la diferencia cuantitativamente.
Resumen en un párrafo¶
La cuantización es un mapa afín q = round(x/s) + z con error por elemento acotado por s/2. El error es pequeño cuando s es pequeña, y s es pequeña cuando la unidad de escalado (por-tensor, por-canal, por-grupo) se ajusta a la distribución local. Los outliers hinchan s y destruyen resolución; el remedio son escalas (scales) de grano fino, no más bits. Para la Fase 26 usamos INT8 simétrico por-canal sobre pesos e INT8 asimétrico estático sobre activaciones como configuración por defecto, con INT4 por-grupo como ajuste de 4 bits. El siguiente fichero de teoría muestra cómo GPTQ mejora INT4 por-grupo explotando las estadísticas de activación.
Siguiente: theory/03-gptq-and-nf4.md.