4048936067
ruff estaba configurado en pyproject.toml desde el primer commit y jamás se había corrido. Tenía 15 hallazgos. Dos importan más allá del estilo: - zip() sin strict= trunca en silencio al más corto. En las comparaciones de lotes eso significa que un test podía pasar sin haber comparado todo. Donde los largos deben coincidir ahora es strict=True; donde difieren a propósito (pares consecutivos) queda strict=False, que documenta la intención. - Un import sin usar delataba algo peor: BraveBackend se había escrito sin una sola prueba. Se agregan ocho, contra una respuesta con la forma que devuelve la API, incluidas la limpieza de etiquetas, el caso de límite de tasa —que tiene que distinguirse de 'no respondió'— y que la credencial viaje en la cabecera y nunca en la URL. El respaldo tiene que funcionar justo cuando el primario ya falló; merecía la misma cobertura. El resto es orden de imports, collections.abc y líneas largas.
130 lines
3.9 KiB
Python
130 lines
3.9 KiB
Python
"""La config tiene que fallar al arrancar, no a las tres horas."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from enlace.config.load import ConfigError, compose, load_config
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def test_composicion_por_capas():
|
|
cfg = load_config(REPO / "configs/runs/pretrain-2060.yaml")
|
|
assert cfg.hardware.name == "turing-2060"
|
|
assert cfg.model.name == "tiny-50m"
|
|
assert cfg.train.run_name == "pretrain-tiny-50m"
|
|
assert cfg.data.source == "shards"
|
|
|
|
|
|
def test_el_yaml_raiz_pisa_las_capas_incluidas():
|
|
cfg = load_config(REPO / "configs/runs/smoke-2060.yaml")
|
|
# El perfil de la 2060 declara micro_batch_size 8; la corrida lo sube a 64
|
|
# porque el modelo char es diminuto.
|
|
assert cfg.hardware.micro_batch_size == 64
|
|
assert cfg.hardware.dtype == "float16" # esto sí viene del perfil
|
|
|
|
|
|
def test_overrides_de_linea_de_comandos():
|
|
cfg = load_config(REPO / "configs/runs/smoke-cpu.yaml", ["train.seed=99"])
|
|
assert cfg.train.seed == 99
|
|
|
|
|
|
@pytest.mark.parametrize("perfil", ["turing-2060", "blackwell-5090", "cpu"])
|
|
def test_todos_los_perfiles_de_hardware_son_validos(perfil, tmp_path):
|
|
"""Un perfil roto solo se descubre al migrar de placa; mejor ahora."""
|
|
raiz = {
|
|
"include": {
|
|
"hardware": f"hardware/{perfil}.yaml",
|
|
"model": "model/tiny-50m.yaml",
|
|
"train": "train/pretrain.yaml",
|
|
"data": "data/corpus.yaml",
|
|
}
|
|
}
|
|
path = REPO / "configs" / "runs" / f"_tmp_{perfil}.yaml"
|
|
path.write_text(yaml.safe_dump(raiz))
|
|
try:
|
|
cfg = load_config(path)
|
|
assert cfg.hardware.name == perfil
|
|
finally:
|
|
path.unlink()
|
|
|
|
|
|
def _raiz(tmp_path: Path, **parches) -> Path:
|
|
"""Config raíz válida con parches encima, para probar validaciones."""
|
|
base = {
|
|
"include": {
|
|
"hardware": "hardware/cpu.yaml",
|
|
"model": "model/tiny-50m.yaml",
|
|
"train": "train/pretrain.yaml",
|
|
"data": "data/corpus.yaml",
|
|
}
|
|
}
|
|
base.update(parches)
|
|
path = REPO / "configs" / "runs" / "_tmp_test.yaml"
|
|
path.write_text(yaml.safe_dump(base))
|
|
return path
|
|
|
|
|
|
def _espera_error(path: Path, fragmento: str):
|
|
try:
|
|
with pytest.raises(ConfigError) as exc:
|
|
load_config(path)
|
|
assert fragmento in str(exc.value)
|
|
finally:
|
|
path.unlink()
|
|
|
|
|
|
def test_rechaza_campos_desconocidos(tmp_path):
|
|
# Un typo tiene que ser un error ruidoso, no un default silencioso.
|
|
_espera_error(_raiz(tmp_path, model={"n_layers": 12}), "n_layers")
|
|
|
|
|
|
def test_rechaza_fp16_sin_grad_scaler(tmp_path):
|
|
_espera_error(
|
|
_raiz(tmp_path, hardware={"dtype": "float16", "use_grad_scaler": False}),
|
|
"use_grad_scaler",
|
|
)
|
|
|
|
|
|
def test_rechaza_grad_scaler_sin_fp16(tmp_path):
|
|
_espera_error(
|
|
_raiz(tmp_path, hardware={"dtype": "float32", "use_grad_scaler": True}),
|
|
"use_grad_scaler",
|
|
)
|
|
|
|
|
|
def test_rechaza_dmodel_no_divisible_por_heads(tmp_path):
|
|
_espera_error(_raiz(tmp_path, model={"d_model": 500}), "no es divisible")
|
|
|
|
|
|
def test_rechaza_gqa_incoherente(tmp_path):
|
|
_espera_error(_raiz(tmp_path, model={"n_head": 8, "n_kv_head": 3}), "n_kv_head")
|
|
|
|
|
|
def test_rechaza_schedule_que_no_entra(tmp_path):
|
|
_espera_error(
|
|
_raiz(
|
|
tmp_path,
|
|
train={"max_steps": 100, "schedule": {"warmup_steps": 80, "decay_steps": 80}},
|
|
),
|
|
"no quedaría fase estable",
|
|
)
|
|
|
|
|
|
def test_rechaza_data_source_sin_su_ruta(tmp_path):
|
|
_espera_error(_raiz(tmp_path, data={"source": "chars", "text_path": None}), "text_path")
|
|
|
|
|
|
def test_include_con_capa_desconocida(tmp_path):
|
|
path = REPO / "configs" / "runs" / "_tmp_bad.yaml"
|
|
path.write_text(yaml.safe_dump({"include": {"hardwar": "hardware/cpu.yaml"}}))
|
|
try:
|
|
with pytest.raises(ConfigError, match="capas desconocidas"):
|
|
compose(path)
|
|
finally:
|
|
path.unlink()
|