English · Español
Break — Quitar PYTHONHASHSEED de seed_everything¶
Pequeña ruptura, gran lección: si
PYTHONHASHSEEDno se fija (o se fija demasiado tarde),hash(str)cambia entre procesos y los dataloaders que indexan por hash producen orden distinto en cada ejecución. La reproducibilidad muere en silencio.
Este ejercicio /break apunta a la higiene de reproducibilidad. Es intencionadamente sutil: la suite de tests puede seguir pasando; el fallo aparece solo al reiniciar procesos.
Hipótesis¶
El learner predice: "Si quito la línea os.environ['PYTHONHASHSEED'] de seed_everything, mis tests unitarios seguirán pasando porque random y numpy siguen sembrados — pero cualquier código que dependa del orden de hash(str) entre procesos Python frescos divergirá en silencio."
La ruptura¶
En src/utils/seeding.py, comenta la línea que fija PYTHONHASHSEED:
def seed_everything(seed: int) -> None:
- os.environ["PYTHONHASHSEED"] = str(seed)
+ # os.environ["PYTHONHASHSEED"] = str(seed) # /break: removed
random.seed(seed)
np.random.seed(seed)
...
Procedimiento de ejecución¶
Ejecuta esto dos veces como procesos Python separados (no dos llamadas en el mismo REPL):
uv run python -c "
from src.utils.seeding import seed_everything
seed_everything(0)
print(sorted({'work','play','walk','study','listen'}, key=hash))
"
# Ejecuta el mismo comando de nuevo. Compara las dos salidas.
En un control paralelo, fija la variable de entorno a nivel del launcher:
PYTHONHASHSEED=0 uv run python -c "
print(sorted({'work','play','walk','study','listen'}, key=hash))
"
# Repite. Las salidas ahora deberían coincidir.
Modo de fallo esperado¶
- Sin el fix: las dos invocaciones de proceso producen distintos ordenamientos del set de verbos. Las salidas de
randomynumpysiguen coincidiendo (porque esas están sembradas dentro del proceso), así que un test unitario ingenuo pasaría. - Con la variable de entorno a nivel del launcher: las salidas coinciden entre procesos.
Firma cuantitativa: en 10 trials con sets de 5 strings, la probabilidad esperada de que dos arranques aleatorios de Python produzcan el mismo orden hash de 5 strings es 1/5! = 1/120 ≈ 0.83%. En la práctica verás desacuerdo en 1–2 trials.
Diagnóstico¶
Desde los logs solos, el síntoma es "mi dataloader produce un orden de muestra distinto en cada reinicio, aunque seed_everything(0) es lo primero que llamo". El chequeo definitivo:
echo $PYTHONHASHSEED # vacío cuando está roto
uv run python -c "import os; print(os.environ.get('PYTHONHASHSEED', 'unset'))"
Si la variable de entorno está sin fijar al arranque del proceso pero os.environ['PYTHONHASHSEED'] se fija dentro del proceso, no afecta retroactivamente a hash(str) — el hash randomizer a nivel C se inicializa antes de que cualquier código Python corra.
Lección¶
PYTHONHASHSEED es especial: debe fijarse en el entorno del shell que lanza, no dentro del script. El wrapper de receta just y el test harness lo exportan ambos; llamar a seed_everything dentro del script es higiénico pero no suficiente. Documentar esta advertencia en el docstring de la función (y en la entrada de diario del día) es el entregable real; el estado roto enseñó la razón.
Referencia¶
- Docs de CPython,
PYTHONHASHSEEDysys.flags.hash_randomization. - PEP 456 — Secure and interchangeable hash algorithm (background sobre por qué existe hash randomization en primer lugar).