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.
Sin terminal, Python retiene la salida estándar, así que el comando de registro
no mostraba nada durante minutos aunque el entrenamiento estuviera avanzando —
parecía colgado. Se comprobó comparando el archivo de registro, atrasado,
contra las métricas, que se escriben sin buffer y sí avanzaban.
Cadena de búsqueda con respaldo. DuckDuckGo sigue de primario porque no exige
credenciales: en el camino habitual ningún tercero se entera de qué busca la
familia. Pero su endpoint lite no es una API con contrato, así que cuando
limite por tasa o cambie el HTML, Brave responde.
Dos matices del encadenado:
- Cero resultados NO dispara el respaldo. Si el primario respondió bien y no
encontró nada, esa es la respuesta correcta; encadenar gastaría cuota y
devolvería resultados peores. Solo se avanza ante un error real.
- La cadena recuerda quién respondió: al depurar una respuesta rara, lo
primero que hay que saber es de dónde salió.
Un respaldo sin credencial falla al arrancar y no en la primera consulta —
que es justo cuando el primario ya falló y el respaldo tiene que funcionar.
Aparte, verificando torch en la 2060 apareció un defecto real en backends.py:
torch.cuda.is_bf16_supported() usa including_emulation=True por defecto, así
que devuelve True en Turing, donde bf16 no existe en silicio. La guardia no
habría atrapado el perfil de la 5090 corriendo en la 2060: el entrenamiento
seguía, emulado y silencioso. Medido en la placa: 4,49 ms por matmul en bf16
contra 1,75 ms en fp16, unas 2,6x más lento. Ahora se consulta con
including_emulation=False, con respaldo por compute capability.
11 tests nuevos (113 en total).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base del proyecto ENLACE: un modelo de lenguaje propio entrenado desde cero,
en español, para asistencia general y familiar. El plan completo está en
docs/PLAN.md.
Esta etapa establece el andamiaje y lo verifica de punta a punta:
- Configuración por capas (hardware × model × train × data) validada con
pydantic. Ningún hiperparámetro vive en el código y una config inválida
falla al arrancar, no a las tres horas de entrenamiento.
- Perfiles de hardware que aíslan el salto de GPU: la RTX 2060 (Turing) no
soporta bfloat16 ni FlashAttention-2, así que entrena en float16 con
GradScaler y backend mem_efficient; el perfil de la 5090 ya está escrito.
backends.py valida el perfil contra la GPU real antes de empezar.
- Transformer decoder-only estilo Llama: RMSNorm, SwiGLU, RoPE, GQA,
embeddings atados, QK-norm y z-loss. Los dos últimos son lo que mantiene
estable el entrenamiento en float16.
- Entrenador con schedule WSD, acumulación de gradiente, precisión mixta,
checkpointing atómico y reanudación exacta.
- Cargadores de datos con estado serializable: bytes para el smoke test y
shards uint16 para el corpus real.
48 tests, entre ellos el crítico: reanudar desde un checkpoint reproduce los
pesos de una corrida ininterrumpida, parámetro por parámetro.
Verificado en CPU: 300 pasos sobre texto en español, loss 3.07 -> 1.63.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>