Skip to content

English · Español

Lab 03 — Ida y vuelta fp32 ↔ int8 y medir la pérdida

Objetivo: cuantificar el error introducido por la cuantización INT8 simétrica sobre una distribución típica de activaciones. Anticipar la Fase 26 sin hacer calibración real.

Tiempo estimado: 45–75 minutos.

Prerrequisito: teoría 04-precision-zoo.md § "Formatos enteros" leída.


Lo que produces

Un directorio experiments/02-quantization-preview/ que contiene:

  • quant.pyquantize_fp32_to_int8(x, s) y dequantize_int8_to_fp32(q, s) más un pequeño helper para elegir s.
  • experiment.py — driver que ejecuta la ida y vuelta sobre tres distribuciones de test y registra errores.
  • results.json — estadísticas de error por distribución.
  • error_histogram.png — distribución de errores absolutos por elemento.
  • manifest.json.
  • README.md — interpretación, incluyendo la pregunta "¿es esto suficiente para el clasificador de tiempos de §A13?".

El encuadre de §A13

Las distribuciones de test que cuantizarás son ejemplos de lo que un modelo pequeño produce para la tarea de §A13:

  1. Logits del clasificador de tiempos. Un vector de longitud 5 con valores en [-3, 5] (logits típicos pre-softmax para la clasificación de 5 tiempos).
  2. Masa de la distribución de formas verbales. Un vector de longitud 600 representando la probabilidad predicha por el modelo sobre las 600 formas verbales. Valores en [0, 0.1], con la mayoría de valores < 0.01.
  3. Activaciones ocultas. Un vector de longitud 1024 con valores aproximadamente Normal(0, 1) — un estado oculto típico de un transformer pequeño.

Para cada uno, preguntamos: ¿cuánta precisión cuesta la ida y vuelta INT8?

TODOs

Bloque A — implementaciones

quant.py:

def choose_scale_symmetric(x, n_bits=8):
    # INT8 symmetric: representable range is [-127, 127]
    # Scale so that x.max() / s ≤ 127 and -x.max() / s ≥ -127
    # i.e., s = abs(x).max() / 127
    return np.abs(x).max() / 127.0

def quantize_fp32_to_int8(x, s):
    # Round to nearest, clip to [-128, 127]
    q = np.clip(np.round(x / s), -128, 127).astype(np.int8)
    return q

def dequantize_int8_to_fp32(q, s):
    return q.astype(np.float32) * s

Variante: implementa también una versión asimétrica con un punto cero z:

def choose_scale_asymmetric(x, n_bits=8):
    qmin, qmax = -128, 127
    s = (x.max() - x.min()) / (qmax - qmin)
    z = qmin - np.round(x.min() / s).astype(np.int32)
    return s, z

def quantize_asym(x, s, z):
    return np.clip(np.round(x / s + z), -128, 127).astype(np.int8)

def dequantize_asym(q, s, z):
    return (q.astype(np.float32) - z) * s

Bloque B — generar las tres distribuciones

rng = np.random.default_rng(42)

# Distribution 1: tense logits, length 5
logits = np.array([1.2, 4.7, 3.1, 0.5, 2.9], dtype=np.float32)
# Or, generate a batch: rng.uniform(-3, 5, size=(64, 5)).astype(np.float32)

# Distribution 2: verb-form probabilities, length 600
# Highly skewed: a few large, most tiny
probs_raw = np.exp(rng.normal(0, 1.5, 600)).astype(np.float32)
probs = probs_raw / probs_raw.sum()

# Distribution 3: hidden activations, length 1024
hidden = rng.standard_normal(1024).astype(np.float32)

Cada distribución se guarda en experiment.py como un tensor con nombre.

Bloque C — ida y vuelta y medición

Para cada distribución, con cuantización tanto simétrica como asimétrica:

  1. Calcula la escala (y el punto cero para asimétrica).
  2. Cuantiza a int8.
  3. Descuantiza de vuelta a fp32.
  4. Registra:
  5. Error absoluto medio: np.abs(x - x_dequant).mean().
  6. Error absoluto máximo.
  7. Error relativo medio (donde |x| > some_threshold para evitar la división por valores diminutos): np.abs((x - x_dequant) / x).mean().
  8. Similitud coseno entre x y x_dequant.
  9. Número de valores int8 únicos usados de 256 (utilización).

Guarda en results.json como una tabla:

{
  "tense_logits": {
    "symmetric":  {"mae": ..., "max_err": ..., "rel_err": ..., "cos_sim": ..., "n_codes": ...},
    "asymmetric": { ... }
  },
  "verb_probs":   { ... },
  "hidden":       { ... }
}

Bloque D — predecir antes de medir

En README.md, antes de ejecutar, predice:

  • ¿Qué distribución se cuantizará a INT8 con más precisión, y por qué?
  • ¿Qué distribución perderá información de forma más dolorosa?
  • Para las probabilidades de formas verbales (donde la mayoría de valores son < 0.01): ¿cuál es el paso de cuantización s, y cuál es el error relativo sobre un valor de 0.001?

Razonamiento esperado: la escala la fija el max, así que distribuciones con alto rango dinámico (probabilidades verbales: de 0 a ~0.5) obtienen un paso grueso s ≈ 0.5/127 ≈ 0.004. Los valores de 0.001 redondean o a 0 (q = 0) o a 1 × s = 0.004 — error relativo de 100–300%. INT8 es brutal para la distribución de probabilidades de formas verbales. La asimétrica ayuda un poco (usa los códigos negativos para nada aquí, así que sin ganancia). Escalas por canal o cuantización en dominio logarítmico (Fase 26) ayudarían.

Para logits de tiempos ([-3, 5]): paso s ≈ 5/127 ≈ 0.039. El error relativo sobre un logit de 1.2 es 0.039/1.2 ≈ 3%. Tolerable. El softmax debería seguir clasificando el mismo tiempo primero.

Para activaciones ocultas (Normal(0, 1)): paso s ≈ 3.5/127 ≈ 0.028 (asumiendo max ≈ 3.5σ). El error relativo sobre un |x| ≈ 0.8 típico es ~3.5%. Tolerable.

Bloque E — la pregunta asesina

Para las probabilidades de formas verbales, tras la ida y vuelta INT8, ¿cambia el ranking de las top-k formas? Calcula np.argsort(probs)[::-1][:10] tanto para la original como para la versión cuantizada-luego-descuantizada. ¿Son las top-10 el mismo conjunto? ¿En el mismo orden?

Esta es la pregunta relevante para la tarea. Cuantización INT8 que preserva probabilidades a 3 cifras significativas es inútil si intercambia las dos predicciones principales. La estabilidad de argsort es una métrica de evaluación de la Fase 26; aquí la estás previsualizando.

Bloque F — histograma

error_histogram.png: para las activaciones ocultas (longitud 1024), grafica un histograma de np.abs(x - x_dequant). Anota el error máximo teórico s/2 ≈ 0.014. Confirma que el histograma alcanza su pico por debajo de eso.

Restricciones

  • s simétrica a partir del valor absoluto máximo. No uses estrategias exóticas (recorte por percentil, calibración). Eso es la Fase 26.
  • Solo NumPy. Nada de bitsandbytes, nada de torch.quantization. La ida y vuelta NumPy puro basta para la medición.
  • Usa el RNG con semilla para reproducibilidad.

Condiciones de parada

Hecho cuando:

  1. Dos implementaciones (simétrica, asimétrica) en quant.py.
  2. Tres distribuciones × ambos modos de cuantización en results.json.
  3. La pregunta de "estabilidad de argsort top-10" se responde para la distribución de probabilidad de formas verbales.
  4. error_histogram.png existe.
  5. README.md hace la recomendación: ¿puede confiarse en INT8 solo para la predicción de formas verbales de §A13? (Respuesta esperada: no, porque la distribución es demasiado sesgada; la Fase 26 lo resolverá con escalas por canal o codificación en dominio logarítmico.)

Escollos

  • np.round de medios. El comportamiento predeterminado es el redondeo del banquero (round half to even). np.round(0.5) = 0.0, no 1.0. Documéntalo si aflora.
  • Recorte asimétrico en la frontera. El cómputo del punto cero puede salirse ligeramente de [-128, 127] para distribuciones extremas; recorta tras calcular.
  • np.abs(x - x_dequant) / x divide por cero cuando una entrada es exactamente cero (raro para datos aleatorios fp32 pero posible). Enmascara con |x| > 1e-8.
  • max de un tensor 2D por batch. Usa el eje correcto. Para cuantización por tensor, x.max() sobre todo. Para cuantización por canal (Fase 26), x.max(axis=-1, keepdims=True).

Cuándo consultar solutions/

Tras commitear los cinco archivos. Solución en solutions/03-quantization-preview-ref.md (escrita al abrir la fase).

Pista de último recurso

Si tu ida y vuelta INT8 produce errores mucho mayores de lo esperado, probablemente olvidaste descuantizar de vuelta a fp32 antes de comparar — es decir, comparando x fp32 con q int8 directamente. El paso de descuantización q.astype(np.float32) * s es esencial.


Laboratorios de la Fase 2 completos. Siguiente: /quiz 02, luego PHASE_02_REPORT.md, luego reflexión, luego proceed a la Fase 3.