From 80691131e391a64f5ff5c15fa900c3e656a6e04a Mon Sep 17 00:00:00 2001 From: Mateo Saldain Date: Tue, 28 Jul 2026 07:14:18 -0300 Subject: [PATCH] =?UTF-8?q?Determinismo=20como=20opci=C3=B3n=20del=20perfi?= =?UTF-8?q?l,=20para=20poder=20verificar=20la=20reanudaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Eso hacía imposible comprobar el criterio de aceptación más importante del entrenamiento — que reanudar reproduzca la corrida — porque cualquier diferencia se confundía con el ruido de la placa. Con deterministic activado, la reanudación resulta exacta en los 47 parámetros: el checkpoint captura todo el estado. Queda como opción del perfil y como una config propia, en vez de un conjuro de shell que hay que recordar. La opción rechaza convivir con la compilación, porque el autotune vuelve a elegir kernels distintos entre corridas y reintroduce justo lo que se quería eliminar. Está apagada en producción: cuesta rendimiento. Co-Authored-By: Claude Opus 5 --- configs/runs/verificar-reanudacion-2060.yaml | 32 ++++++++++++++++++++ enlace/config/schema.py | 21 +++++++++++++ enlace/train/backends.py | 17 +++++++++++ 3 files changed, 70 insertions(+) create mode 100644 configs/runs/verificar-reanudacion-2060.yaml 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":