diff --git a/configs/runs/verificar-reanudacion-2060.yaml b/configs/runs/verificar-reanudacion-2060.yaml new file mode 100644 index 0000000..4c4b594 --- /dev/null +++ b/configs/runs/verificar-reanudacion-2060.yaml @@ -0,0 +1,32 @@ +# Verifica que reanudar desde un checkpoint reproduzca la corrida bit a bit. +# +# Es el criterio de aceptación más importante del entrenamiento: una reanudación +# que no es exacta reentrena sobre datos ya vistos y arruina la corrida en +# silencio. En GPU no se puede comprobar sin determinismo, porque dos corridas +# idénticas ya difieren entre sí por el orden de reducción de los kernels. +# +# No sirve para entrenar: es lento a propósito. +include: + hardware: hardware/turing-2060.yaml + model: model/char-smoke.yaml + train: train/smoke.yaml + data: data/smoke.yaml + +hardware: + micro_batch_size: 64 + grad_accum_steps: 1 + deterministic: true + compile: false # el autotune reintroduce variación entre corridas + +train: + run_name: verificar-reanudacion + max_steps: 20 + checkpoint_every: 10 + log_every: 999 + eval_every: 999 + sample_every: 0 + schedule: + kind: wsd + warmup_steps: 2 + decay_steps: 2 + min_lr_ratio: 0.0 diff --git a/enlace/config/schema.py b/enlace/config/schema.py index 3595a43..b692e7f 100644 --- a/enlace/config/schema.py +++ b/enlace/config/schema.py @@ -40,6 +40,16 @@ class HardwareConfig(_Base): compile: bool = True matmul_precision: Literal["highest", "high", "medium"] = "high" + # Determinismo bit a bit. Apagado en producción a propósito: cuesta + # rendimiento y obliga a desactivar la compilación. + # + # Existe porque en GPU dos corridas idénticas NO dan los mismos pesos — el + # orden de reducción de los kernels varía. Medido en la RTX 2060, dos + # corridas iguales de 20 pasos difieren hasta 8,6e-4. Sin esto es imposible + # verificar que la reanudación sea exacta: cualquier diferencia se confunde + # con el ruido de la placa. + deterministic: bool = False + # El tamaño de lote es una propiedad de la placa, no del modelo: 12 GB y # 32 GB no admiten lo mismo. El lote efectivo es micro_batch * grad_accum. micro_batch_size: int = Field(gt=0) @@ -59,6 +69,17 @@ class HardwareConfig(_Base): def effective_batch_size(self) -> int: return self.micro_batch_size * self.grad_accum_steps + @model_validator(mode="after") + def _check_determinismo(self) -> HardwareConfig: + # El autotune de torch.compile elige kernels distintos entre corridas, + # así que reintroduce el no-determinismo que se quiso eliminar. + if self.deterministic and self.compile: + raise ValueError( + f"perfil '{self.name}': deterministic=true exige compile=false " + "(el autotune de la compilación vuelve a hacer variar los kernels)." + ) + return self + @model_validator(mode="after") def _check_dtype_scaler(self) -> HardwareConfig: # float16 sin GradScaler diverge; bfloat16 con GradScaler es un diff --git a/enlace/train/backends.py b/enlace/train/backends.py index d9ea38b..ca46ab3 100644 --- a/enlace/train/backends.py +++ b/enlace/train/backends.py @@ -8,6 +8,8 @@ conviene arreglarla acá en vez de propagarla. from __future__ import annotations +import os + import torch from enlace.config.schema import HardwareConfig @@ -100,9 +102,24 @@ def setup_device(cfg: HardwareConfig) -> torch.device: ) torch.set_float32_matmul_precision(cfg.matmul_precision) + if cfg.deterministic: + _activar_determinismo() return torch.device("cuda") +def _activar_determinismo() -> None: + """Fuerza kernels deterministas, para poder verificar la reanudación. + + `CUBLAS_WORKSPACE_CONFIG` se define acá y no en el shell porque cuBLAS lo + lee al inicializarse, y en este punto todavía no se tocó la placa. Definirlo + después no tiene efecto y el error que da es opaco. + """ + os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8") + torch.use_deterministic_algorithms(True) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + def autocast_context(cfg: HardwareConfig, device: torch.device): """Contexto de precisión mixta acorde al perfil.""" if cfg.dtype == "float32":