English · Español
Lab 00 — Decodificar floats a mano y por código¶
Objetivo: interiorizar la disposición de bits de fp32 escribiendo un decodificador que produzca la misma salida que tu trabajo a lápiz.
Tiempo estimado: 60–90 minutos.
Lo que produces¶
Un directorio experiments/02-float-anatomy/ que contiene:
decode.py— un script de Python que implementadecode_fp32(x: float) -> dictyencode_fp32(s, e, m) -> floata partir de los campos de bits crudos.worked.md— tus respuestas decodificadas a mano para los cuatro valores objetivo (ver TODOs).bf16_round_trip.py— emulación de bf16 vía bit-cast.fp16_round_trip.py— ida y vuelta de fp16 víanumpy.float16.manifest.json— semilla, versiones, hardware, configuración.README.md— interpretación.
TODOs¶
Bloque A — decodificar a mano¶
Para cada uno de estos cuatro valores, decodifica a mano en (sign, biased_exponent, mantissa_bits, value) y escribe el resultado en worked.md. No uses código todavía.
- El patrón de bits fp32
0x3F800000. - El patrón de bits fp32
0x40490FDB(puede que lo reconozcas). - La representación fp32 de
1/600(la probabilidad uniforme del vocabulario de §A13). Para encontrarla, puedes usar el procedimiento inverso: escribe1/600en binario, normaliza, lee el signo / exponente / mantisa. Muestra tu trabajo. - La representación fp32 de
92.0(un logit de magnitud adversaria de un clasificador de tiempos — ver teoría02).
🇪🇸 Truco:
1/600 ≈ 0.001\overline{6}. Normaliza al rango[1, 2):1/600 = 1.70\overline{6} \times 2^{-10}(verifícalo:1.7066... / 1024 ≈ 0.00166...). La parte después del primer1.es tu mantisa.
Bloque B — implementar decode_fp32 y encode_fp32¶
Dos funciones. Python puro (se permite el módulo struct; nada de NumPy).
decode_fp32(x: float) -> dict devuelve:
{
"raw_bits": "0x...",
"sign": 0 or 1,
"biased_exponent": int in [0, 255],
"unbiased_exponent": int (biased_exponent - 127),
"mantissa_bits": "0b... (23-bit string)",
"is_normal": bool,
"is_denormal": bool,
"is_zero": bool,
"is_inf": bool,
"is_nan": bool,
"reconstructed_value": float
}
encode_fp32(sign, biased_exponent, mantissa) -> float es la inversa.
Restricciones:
- Usa
struct.pack('<f', x)para obtener los bits fp32,struct.unpack('<I', ...)para leer como uint32. - Enmascara y desplaza para extraer campos. Nada de atajos con
numpy.frombuffer— el punto es escribir la manipulación de bits explícitamente. reconstructed_valuedebe coincidir conxpor identidad de bits para cualquier entrada finita. Testea con:±0, denormales (1e-40),1.0,1/600,92.0,inf,-inf,nan.
Bloque C — verificar que el trabajo a mano coincide con el código¶
Para cada uno de los cuatro valores en el Bloque A, ejecuta tu decode_fp32 y compara con tu trabajo a mano. Deben coincidir exactamente. Cualquier discrepancia → arregla el trabajo a mano; si ambos concuerdan pero no estás seguro, escribe la discrepancia y razona sobre ella en worked.md.
Bloque D — emulación de bf16¶
bf16_round_trip.py: implementa
def to_bf16(x: float) -> int:
# returns 16-bit integer (the bf16 bit pattern)
...
def from_bf16(bits: int) -> float:
# returns fp32 value
...
def round_trip_bf16(x: float) -> float:
return from_bf16(to_bf16(x))
Estrategia: fp32-a-bf16 es "tomar los 16 bits altos del patrón de bits fp32" (con redondeo apropiado — round-half-to-even, pero para este laboratorio el truncamiento simple está bien y puedes anotar el sesgo). bf16-a-fp32 es "desplazar a la izquierda los 16 bits a un hueco de 32 bits, poner a cero los 16 inferiores".
Haz ida y vuelta con estos valores:
1.01/60092.00.1- La probabilidad
1e-20
Imprime: (original_fp32, bf16_bits, recovered_fp32, relative_error).
Bloque E — ida y vuelta de fp16¶
fp16_round_trip.py: usa np.float16 para la conversión (es IEEE-754 fp16, correcto en bits). Haz ida y vuelta con los mismos cinco valores. Imprime la misma tabla.
Observa: ¿92.0 sobrevive a bf16 (rango hasta ~3.4e38) pero desborda a +inf en fp16 (rango hasta ~6.5e4)? En realidad 92.0 está dentro del rango de fp16; el umbral de overflow de fp16 es para exp(92.0), no para 92.0 mismo. Anota esta distinción en tu README.md.
Bloque F — la demostración asesina¶
Muestra, en README.md, con la salida de tus scripts:
Los patrones de bits deben diferir en el bit más bajo. Esta es tu evidencia — no solo una afirmación — de que la aritmética fp es aproximada.
Restricciones¶
- Trabajo a mano primero, código después. El punto de este laboratorio no es escribir un decodificador; es pensar como un decodificador. Si te saltas el trabajo a mano, te estás haciendo trampas a ti mismo.
- Nada de NumPy para
decode_fp32/encode_fp32. Solostruct. Te fuerza a ver los campos de bits explícitamente. - Verifica cada salida. Un decode que imprime
1.0es sospechoso — verifica los campos de bits.
Condiciones de parada¶
Has terminado cuando:
worked.mdcontiene decodificaciones a mano para los cuatro objetivos del Bloque A.decode.pypasa un auto-test:decode_fp32(x)['reconstructed_value'] == xparax in [±0, 1.0, 1/600, 92.0, 0.1, 1e-40, np.inf, np.nan](NaN esis_nan == True, ya quenan != nan).bf16_round_trip.pyproduce la tabla.fp16_round_trip.pyproduce la tabla.README.mdcontiene la evidencia de0.1 + 0.2y una interpretación en un párrafo de qué perdieron bf16 vs fp16 en cada valor.manifest.jsonestá en su sitio.
Escollos¶
- Endianness. Usa
'<f'(little-endian) consistentemente. Mezclar con'>f'corromperá silenciosamente tu salida de cadena de bits. - Manejo de denormales en tu decodificador. Cuando
biased_exponent == 0, el1.líder se vuelve0.y el exponente es1 - bias, no0 - bias. Testea con1e-40. - NaN. Existen muchos patrones de bits NaN (cualquiera con
e == 0xFFym != 0).is_nanno debe comparar contra un patrón específico; comprueba la condición de campo. - Redondeo de bf16. El truncamiento simple sesga hacia cero. El redondeo bf16 "correcto" es round-half-to-even, pero para este laboratorio el truncamiento es aceptable; solo anota el sesgo en
README.md.
Cuándo consultar solutions/¶
Tras commitear los cinco archivos. La solución vive en solutions/00-bit-anatomy-ref.md — escrita al abrir la fase.
Pista de último recurso¶
Si 1/600 te está peleando durante dos horas, aquí tienes la respuesta para comprobar:
sign = 0
exponent = 117 (biased) → unbiased -10
mantissa = 0b10110100111010000001110 = 5927950 (with rounding noise)
reconstructed ≈ 0.0016666667... (off by ~3e-11 from the exact 1/600)
Usa esto solo para verificar, no para copiar. El punto es el procedimiento, no la respuesta.
Siguiente laboratorio: lab/01-softmax-stability.md.