"""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()