English · Español
Solución 03 — referencia de seed_everything¶
Léelo solo tras completar
../lab/03-seed-by-hand.mdy commitear tu intento.
Implementación de referencia¶
src/utils/seeding.py:
"""Deterministic seeding for every RNG in scope.
What this covers:
- PYTHONHASHSEED — dict iteration, hash(str) for new processes
- random — stdlib RNG
- numpy.random — legacy global generator
- torch — CPU + per-CUDA-device + cuDNN deterministic
- log_versions — capture installed library versions for manifests
What it does NOT cover:
- Multi-threaded BLAS reduction order (set OMP_NUM_THREADS=1 explicitly)
- TF32 / FP16 hardware nondeterminism on Ampere+ GPUs
- numpy.random.default_rng(seed) — those generators have their own state
(prefer passing rng around explicitly in your own code; seed_everything
only sets the legacy global state for libraries that haven't migrated)
"""
from __future__ import annotations
import os
import random
import sys
def seed_everything(seed: int) -> None:
"""Seed every reachable RNG with the given integer.
Call this **before** any RNG use. Setting PYTHONHASHSEED here only
affects the current process for code that reads os.environ at runtime;
to make hash(str) deterministic across processes, also set
PYTHONHASHSEED in the launcher shell.
"""
os.environ["PYTHONHASHSEED"] = str(seed)
random.seed(seed)
try:
import numpy as np
except ImportError:
pass
else:
np.random.seed(seed)
try:
import torch
except ImportError:
pass
else:
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
def log_versions() -> dict[str, str]:
"""Return a dict {name: version-or-'not installed'} for the libs we care about."""
versions: dict[str, str] = {"python": sys.version.split()[0]}
for name in ("numpy", "torch", "scipy"):
try:
mod = __import__(name)
except ImportError:
versions[name] = "not installed"
else:
versions[name] = getattr(mod, "__version__", "unknown")
return versions
Decisiones tomadas en esta referencia¶
try/except ImportErrorpor librería: permite aseed_everythingfuncionar en un venv vacío, en una máquina solo-CPU, etc. Los tests lo verifican entests/test_seeding.py.- Cláusula
else:trastry:: más clara que ifs anidados. Se lee como "si el import tuvo éxito, haz el seeding". from __future__ import annotations: deja que los type hints se mantengan en estilo PEP 604 en Python 3.11 incluso antes de fijartarget-version. Seguro barato.np.random.seedsolamente: deliberadamente no intentamos enumerar todas las instanciasnp.random.default_rng(...)— tienen estado independiente por diseño. Prefiere pasar generadores explícitos en tu propio código.torch.manual_seedantes decuda.is_available:manual_seedes barato; hacerlo incondicionalmente está bien y reduce preocupaciones de orden.
Sutilezas a comparar¶
- ¿Escribiste
from numpy.random import seed as np_seed? Funciona pero tira de numpy en tiempo de import deseeding.py— que es malo si un experimento solo-CPU nunca quiere numpy cargado. - ¿Devolviste
__version__directamente singetattr? Algunas libs no la exponen (raro para las que nos interesan, perogetattr(mod, "__version__", "unknown")es defensivo sin ser ruidoso). - ¿Fijaste
cudnn.benchmark = Falsesincudnn.deterministic = True? Ambos son necesarios para determinismo real. - ¿Llamaste
torch.cuda.manual_seed(singular — solo dispositivo actual) en vez demanual_seed_all(cada dispositivo)?
Test de propiedades (acompañante)¶
Añade a tests/test_seeding.py:
import random
from hypothesis import given, strategies as st
from utils.seeding import seed_everything
@given(st.integers(min_value=0, max_value=2**31 - 1))
def test_seed_determinism_stdlib(seed: int) -> None:
seed_everything(seed)
a = [random.random() for _ in range(5)]
seed_everything(seed)
b = [random.random() for _ in range(5)]
assert a == b
@given(st.integers(min_value=0, max_value=2**31 - 1))
def test_seed_determinism_numpy(seed: int) -> None:
np = __import__("numpy")
seed_everything(seed)
a = np.random.rand(5).tolist()
seed_everything(seed)
b = np.random.rand(5).tolist()
assert a == b
El test de determinismo de torch sería similar pero gateado por pytest.importorskip("torch").
Qué enseña realmente este lab¶
Casi seguro escribirás algo cercano a esta implementación al primer intento. La lección no es "¿pusiste el código correcto?". Es:
- ¿Entendiste el scope de
PYTHONHASHSEED? (A nivel de proceso; fijarlo en código no afecta retroactivamente ahash(str)de strings ya en claves dedict.) - ¿Entendiste que
np.random.seed≠np.random.default_rng(seed)? Producen secuencias distintas desde la misma semilla. - ¿Escribiste un test que falla en una versión bugged? Si tu test es
seed_everything(0); assert random.random() != 0, no es un test de determinismo — es un smoke test. El test de propiedades de arriba es correcto porque compara dos ejecuciones sembradas. - ¿Listaste una fuente de no-determinismo que
seed_everythingno puede cubrir? (Orden de reducción de BLAS multihilo, TF32, atomic-add en FP16, fork multi-proceso sin re-sembrar.)
Si tu learners/borja/phase-00/notes/seeding-notes.md cubre esos cuatro puntos honestamente, este lab está hecho.
Cuando comparas y decides "me quedo con la mía"¶
A veces la respuesta al diff es "mi versión está bien para este currículo, aunque la referencia es ligeramente más conservadora". Esa es una llamada válida — anótalo en seeding-notes.md y sigue. El lab no es "haz tu archivo idéntico a la referencia". Es "ser capaz de defender cada línea que escribiste".