Aplicar ruff format a todo el código
Cambio mecánico, sin efecto en el comportamiento: la suite pasa igual antes y después. Va en un commit propio para no tapar los cambios con sentido. Se agregan además dos flujos de verificación que corren en cada carga al repositorio: - YAML: yamllint para sintaxis y estilo, más la carga de cada config contra su esquema de pydantic. Son cosas distintas — un YAML puede ser sintácticamente perfecto y estar roto igual, con 'run_nombre' en vez de 'run_name'. Ese paso no instala torch: se verificó que la capa de configuración no lo importa, así que corre en segundos en vez de descargar dos gigas y medio de CUDA. - Python: ruff check, ruff format --check y la suite completa con torch de CPU. Las rutas ignoradas de .yamllint.yml van ancladas con barra inicial. Sin anclar, 'data/' y 'runs/' excluían configs/data/ y configs/runs/ — siete archivos, justo los que más importa revisar — y el linter pasaba en verde sin haber mirado nada. Es el mismo defecto que ya había aparecido en .gitignore.
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
# Calidad del código Python, en cada carga.
|
||||||
|
#
|
||||||
|
# Existe por una razón concreta: ruff estaba configurado en pyproject.toml desde
|
||||||
|
# el primer commit y nunca se había ejecutado. Cuando por fin se corrió tenía 15
|
||||||
|
# hallazgos, uno de ellos real —`zip()` sin `strict=` trunca en silencio al más
|
||||||
|
# corto, así que un test que compara dos listas de distinto largo pasaría sin
|
||||||
|
# comparar todo— y otro que delataba un backend sin ninguna prueba.
|
||||||
|
#
|
||||||
|
# Una regla que no se ejecuta no es una regla.
|
||||||
|
name: Python
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
calidad:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Descargar el repositorio
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Preparar Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Instalar ruff
|
||||||
|
run: python -m pip install --upgrade pip "ruff>=0.6"
|
||||||
|
|
||||||
|
- name: Estilo y errores
|
||||||
|
# Las reglas viven en pyproject.toml, no acá: un solo lugar donde
|
||||||
|
# cambiarlas, y el mismo resultado corriendo `ruff check .` en local.
|
||||||
|
run: ruff check --output-format=github .
|
||||||
|
|
||||||
|
- name: Formato consistente
|
||||||
|
run: ruff format --check --diff .
|
||||||
|
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Descargar el repositorio
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Preparar Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Instalar dependencias
|
||||||
|
# torch de CPU explícitamente: la variante con CUDA pesa unos 2,5 GB y
|
||||||
|
# acá no hay placa. La suite entera corre en CPU a propósito.
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||||
|
python -m pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Suite de tests
|
||||||
|
# Se desactiva la carga automática de plugins: en entornos con otros
|
||||||
|
# paquetes instalados (ROS, por ejemplo) pytest intenta cargar sus
|
||||||
|
# plugins y falla al recolectar por motivos ajenos al proyecto.
|
||||||
|
env:
|
||||||
|
PYTEST_DISABLE_PLUGIN_AUTOLOAD: "1"
|
||||||
|
run: pytest -q
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Verificación de los YAML del repositorio, en cada carga.
|
||||||
|
#
|
||||||
|
# Dos niveles, porque atrapan cosas distintas:
|
||||||
|
#
|
||||||
|
# 1. yamllint mira la sintaxis y el estilo: indentación, duplicados, tabs.
|
||||||
|
# 2. La validación contra el esquema carga cada config con pydantic. Un YAML
|
||||||
|
# puede ser sintácticamente perfecto y estar roto igual — `n_layers` en vez
|
||||||
|
# de `n_layer`, un schedule que no entra en max_steps, float16 sin
|
||||||
|
# GradScaler. Eso solo lo ve el esquema.
|
||||||
|
#
|
||||||
|
# El segundo paso no instala torch a propósito: la capa de configuración no lo
|
||||||
|
# importa, así que el CI corre en segundos en vez de descargar 2,5 GB de CUDA.
|
||||||
|
name: YAML
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
yaml:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Descargar el repositorio
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Preparar Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Instalar dependencias
|
||||||
|
# Solo lo que hace falta: yamllint para la sintaxis, pydantic y
|
||||||
|
# omegaconf para el esquema. Nada de torch.
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install "yamllint>=1.35" "pydantic>=2.7" "omegaconf>=2.3"
|
||||||
|
|
||||||
|
- name: Sintaxis y estilo de los YAML
|
||||||
|
# --strict convierte las advertencias en errores: si una regla no vale
|
||||||
|
# la pena, se desactiva en .yamllint.yml, no se deja avisando para
|
||||||
|
# siempre.
|
||||||
|
run: yamllint --strict .
|
||||||
|
|
||||||
|
- name: Las configs cargan y pasan su esquema
|
||||||
|
run: python scripts/validar_configs.py
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Reglas de estilo para los YAML del repositorio.
|
||||||
|
#
|
||||||
|
# Se parte de las reglas por defecto de yamllint y se ajusta solo lo que choca
|
||||||
|
# con las convenciones ya establecidas del proyecto.
|
||||||
|
extends: default
|
||||||
|
|
||||||
|
rules:
|
||||||
|
# 100 caracteres, igual que ruff en pyproject.toml. Los comentarios de las
|
||||||
|
# configs explican decisiones y no entran cómodos en 80.
|
||||||
|
line-length:
|
||||||
|
max: 100
|
||||||
|
allow-non-breakable-words: true
|
||||||
|
|
||||||
|
# No se usa el marcador `---` al inicio de los archivos.
|
||||||
|
document-start: disable
|
||||||
|
|
||||||
|
# `on:` en los workflows es una clave legítima, no el booleano de YAML 1.1.
|
||||||
|
truthy:
|
||||||
|
check-keys: false
|
||||||
|
|
||||||
|
# Las listas dentro de un mapeo se escriben sin indentar de más:
|
||||||
|
# fallbacks:
|
||||||
|
# - brave
|
||||||
|
# y también en línea, `fallbacks: [brave]`. Ambas se aceptan.
|
||||||
|
indentation:
|
||||||
|
spaces: 2
|
||||||
|
indent-sequences: consistent
|
||||||
|
|
||||||
|
comments:
|
||||||
|
min-spaces-from-content: 1
|
||||||
|
|
||||||
|
# Los archivos generados por herramientas externas no siempre traen salto
|
||||||
|
# final; no vale la pena fallar el CI por eso.
|
||||||
|
new-line-at-end-of-file: enable
|
||||||
|
|
||||||
|
# Anclados con "/" inicial a propósito: sin anclar, "data/" y "runs/" excluyen
|
||||||
|
# cualquier directorio con ese nombre en cualquier nivel — incluidos
|
||||||
|
# configs/data/ y configs/runs/, que son justamente los que más importa
|
||||||
|
# revisar. El síntoma es un linter que pasa en verde sin haber mirado nada.
|
||||||
|
ignore: |
|
||||||
|
/.venv/
|
||||||
|
/data/
|
||||||
|
/runs/
|
||||||
|
/snapshots/
|
||||||
|
/checkpoints/
|
||||||
@@ -235,9 +235,7 @@ class SearchConfig(_Base):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
backend: Literal["duckduckgo", "brave", "searxng"] = "duckduckgo"
|
backend: Literal["duckduckgo", "brave", "searxng"] = "duckduckgo"
|
||||||
fallbacks: list[Literal["duckduckgo", "brave", "searxng"]] = Field(
|
fallbacks: list[Literal["duckduckgo", "brave", "searxng"]] = Field(default_factory=list)
|
||||||
default_factory=list
|
|
||||||
)
|
|
||||||
region: str = "es-es"
|
region: str = "es-es"
|
||||||
country: str = "uy"
|
country: str = "uy"
|
||||||
language: str = "es"
|
language: str = "es"
|
||||||
|
|||||||
@@ -56,9 +56,7 @@ class ByteStream:
|
|||||||
)
|
)
|
||||||
raw = np.frombuffer(path.read_bytes(), dtype=np.uint8)
|
raw = np.frombuffer(path.read_bytes(), dtype=np.uint8)
|
||||||
if len(raw) < seq_len * 4:
|
if len(raw) < seq_len * 4:
|
||||||
raise ValueError(
|
raise ValueError(f"{path} tiene {len(raw)} bytes: muy poco para seq_len={seq_len}.")
|
||||||
f"{path} tiene {len(raw)} bytes: muy poco para seq_len={seq_len}."
|
|
||||||
)
|
|
||||||
|
|
||||||
split_at = int(len(raw) * (1.0 - val_fraction))
|
split_at = int(len(raw) * (1.0 - val_fraction))
|
||||||
self._data = {
|
self._data = {
|
||||||
|
|||||||
@@ -186,9 +186,7 @@ class Transformer(nn.Module):
|
|||||||
total -= self.tok_emb.weight.numel()
|
total -= self.tok_emb.weight.numel()
|
||||||
return total
|
return total
|
||||||
|
|
||||||
def forward(
|
def forward(self, idx: Tensor, targets: Tensor | None = None) -> tuple[Tensor, Tensor | None]:
|
||||||
self, idx: Tensor, targets: Tensor | None = None
|
|
||||||
) -> tuple[Tensor, Tensor | None]:
|
|
||||||
"""Devuelve (logits, loss). `loss` es None si no hay targets."""
|
"""Devuelve (logits, loss). `loss` es None si no hay targets."""
|
||||||
_, t = idx.shape
|
_, t = idx.shape
|
||||||
if t > self.cfg.seq_len:
|
if t > self.cfg.seq_len:
|
||||||
|
|||||||
@@ -122,9 +122,7 @@ def load(
|
|||||||
|
|
||||||
fmt = payload.get("format")
|
fmt = payload.get("format")
|
||||||
if fmt != CHECKPOINT_FORMAT:
|
if fmt != CHECKPOINT_FORMAT:
|
||||||
raise ValueError(
|
raise ValueError(f"{path}: formato de checkpoint {fmt}, se esperaba {CHECKPOINT_FORMAT}.")
|
||||||
f"{path}: formato de checkpoint {fmt}, se esperaba {CHECKPOINT_FORMAT}."
|
|
||||||
)
|
|
||||||
|
|
||||||
module = getattr(model, "_orig_mod", model)
|
module = getattr(model, "_orig_mod", model)
|
||||||
module.load_state_dict(payload["model"])
|
module.load_state_dict(payload["model"])
|
||||||
|
|||||||
@@ -146,9 +146,7 @@ def train(cfg: Config, resume: bool = False) -> Path:
|
|||||||
if ckpt_path is None:
|
if ckpt_path is None:
|
||||||
print(f"[enlace] --resume sin checkpoints en {run_dir}: se empieza de cero")
|
print(f"[enlace] --resume sin checkpoints en {run_dir}: se empieza de cero")
|
||||||
else:
|
else:
|
||||||
meta = checkpoint.load(
|
meta = checkpoint.load(ckpt_path, model=model, optimizer=optimizer, scaler=scaler)
|
||||||
ckpt_path, model=model, optimizer=optimizer, scaler=scaler
|
|
||||||
)
|
|
||||||
stream.load_state_dict(meta["stream"])
|
stream.load_state_dict(meta["stream"])
|
||||||
start_step = meta["step"]
|
start_step = meta["step"]
|
||||||
print(f"[enlace] reanudado desde {ckpt_path.name} en el paso {start_step}")
|
print(f"[enlace] reanudado desde {ckpt_path.name} en el paso {start_step}")
|
||||||
@@ -157,9 +155,7 @@ def train(cfg: Config, resume: bool = False) -> Path:
|
|||||||
print("[enlace] compilando el modelo (la primera iteración tarda)...")
|
print("[enlace] compilando el modelo (la primera iteración tarda)...")
|
||||||
model = torch.compile(model) # type: ignore[assignment]
|
model = torch.compile(model) # type: ignore[assignment]
|
||||||
|
|
||||||
tokens_per_step = (
|
tokens_per_step = cfg.hardware.effective_batch_size * cfg.model.seq_len
|
||||||
cfg.hardware.effective_batch_size * cfg.model.seq_len
|
|
||||||
)
|
|
||||||
fpt = flops_per_token(getattr(model, "_orig_mod", model))
|
fpt = flops_per_token(getattr(model, "_orig_mod", model))
|
||||||
model.train()
|
model.train()
|
||||||
|
|
||||||
@@ -194,9 +190,7 @@ def train(cfg: Config, resume: bool = False) -> Path:
|
|||||||
# se aplicaría sobre gradientes inflados por el GradScaler.
|
# se aplicaría sobre gradientes inflados por el GradScaler.
|
||||||
scaler.unscale_(optimizer)
|
scaler.unscale_(optimizer)
|
||||||
grad_norm = float(
|
grad_norm = float(
|
||||||
torch.nn.utils.clip_grad_norm_(
|
torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.train.optimizer.grad_clip)
|
||||||
model.parameters(), cfg.train.optimizer.grad_clip
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
scaler.step(optimizer)
|
scaler.step(optimizer)
|
||||||
scaler.update()
|
scaler.update()
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ distill = [
|
|||||||
dev = [
|
dev = [
|
||||||
"pytest>=8.0",
|
"pytest>=8.0",
|
||||||
"ruff>=0.6",
|
"ruff>=0.6",
|
||||||
|
"yamllint>=1.35",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
Executable
+76
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Valida que todas las configs del repositorio carguen y pasen su esquema.
|
||||||
|
|
||||||
|
Un YAML sintácticamente correcto puede seguir estando roto: `n_layers` en vez de
|
||||||
|
`n_layer`, un schedule que no entra en `max_steps`, float16 sin GradScaler. Eso
|
||||||
|
no lo ve un linter de sintaxis — lo ven los validadores de pydantic, que ya
|
||||||
|
existen. Esto los ejecuta sobre cada config del repositorio.
|
||||||
|
|
||||||
|
No importa torch a propósito: así el CI valida configs en segundos en vez de
|
||||||
|
descargar los dos gigas y medio de CUDA.
|
||||||
|
|
||||||
|
python scripts/validar_configs.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
RAIZ = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(RAIZ))
|
||||||
|
|
||||||
|
from enlace.config.load import ( # noqa: E402
|
||||||
|
ConfigError,
|
||||||
|
load_agent_config,
|
||||||
|
load_config,
|
||||||
|
load_distill_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validar() -> int:
|
||||||
|
fallas: list[tuple[Path, str]] = []
|
||||||
|
verificadas = 0
|
||||||
|
|
||||||
|
# Las configs de corrida componen las cuatro capas, así que validarlas
|
||||||
|
# cubre transitivamente hardware, modelo, entrenamiento y datos.
|
||||||
|
for path in sorted((RAIZ / "configs" / "runs").glob("*.yaml")):
|
||||||
|
try:
|
||||||
|
cfg = load_config(path)
|
||||||
|
verificadas += 1
|
||||||
|
print(
|
||||||
|
f" OK {path.relative_to(RAIZ)} "
|
||||||
|
f"({cfg.hardware.name} / {cfg.model.name} / {cfg.train.run_name})"
|
||||||
|
)
|
||||||
|
except ConfigError as exc:
|
||||||
|
fallas.append((path, str(exc)))
|
||||||
|
print(f" FALLA {path.relative_to(RAIZ)}")
|
||||||
|
|
||||||
|
# Los árboles que no son de entrenamiento se cargan aparte.
|
||||||
|
for etiqueta, cargador, path in (
|
||||||
|
("agente", load_agent_config, RAIZ / "configs" / "agent" / "tools.yaml"),
|
||||||
|
("destilación", load_distill_config, RAIZ / "configs" / "data" / "distill.yaml"),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
cargador(path)
|
||||||
|
verificadas += 1
|
||||||
|
print(f" OK {path.relative_to(RAIZ)} ({etiqueta})")
|
||||||
|
except ConfigError as exc:
|
||||||
|
fallas.append((path, str(exc)))
|
||||||
|
print(f" FALLA {path.relative_to(RAIZ)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
if fallas:
|
||||||
|
for path, motivo in fallas:
|
||||||
|
print(f"--- {path.relative_to(RAIZ)} ---")
|
||||||
|
print(motivo)
|
||||||
|
print()
|
||||||
|
print(f"{len(fallas)} config(s) inválida(s) de {verificadas + len(fallas)}.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"{verificadas} configs válidas.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(validar())
|
||||||
@@ -92,9 +92,7 @@ def test_el_estado_rng_se_restaura_desde_cpu(monkeypatch):
|
|||||||
|
|
||||||
recibidos = []
|
recibidos = []
|
||||||
original = torch.set_rng_state
|
original = torch.set_rng_state
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(torch, "set_rng_state", lambda s: (recibidos.append(s), original(s))[1])
|
||||||
torch, "set_rng_state", lambda s: (recibidos.append(s), original(s))[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
_restore_rng(_rng_state())
|
_restore_rng(_rng_state())
|
||||||
assert recibidos, "no se llamó a set_rng_state"
|
assert recibidos, "no se llamó a set_rng_state"
|
||||||
|
|||||||
Reference in New Issue
Block a user