Etapa 0: entorno, configuración validada, modelo y entrenador

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>
This commit is contained in:
2026-07-27 23:04:45 -03:00
commit 90ac2a6582
33 changed files with 2838 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Descarga un texto en español de dominio público para el smoke test (Etapa 0).
#
# No es corpus de entrenamiento: son unos pocos MB para verificar en minutos que
# CUDA, el entrenador, el checkpointing y la reanudación funcionan antes de
# comprometer días de cómputo.
set -euo pipefail
DESTINO="${1:-data/smoke/texto.txt}"
mkdir -p "$(dirname "$DESTINO")"
if [[ -s "$DESTINO" ]]; then
echo "[enlace] ya existe $DESTINO ($(wc -c <"$DESTINO") bytes); no se baja de nuevo."
exit 0
fi
# Don Quijote — dominio público, español, y suficientemente largo.
URLS=(
"https://www.gutenberg.org/cache/epub/2000/pg2000.txt"
"https://www.gutenberg.org/files/2000/2000-0.txt"
)
for url in "${URLS[@]}"; do
echo "[enlace] bajando $url"
if curl -fsSL --max-time 120 "$url" -o "$DESTINO"; then
echo "[enlace] listo: $DESTINO ($(wc -c <"$DESTINO") bytes)"
exit 0
fi
done
echo "[enlace] no se pudo descargar el texto. Alternativa: copiar cualquier" >&2
echo " archivo .txt en español a $DESTINO (unos pocos MB alcanzan)." >&2
exit 1
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Trabajo contra el servidor de entrenamiento por SSH.
#
# El código viaja por git; los datos, las bases y los checkpoints viven en el
# servidor y no se descargan enteros. Solo vuelven artefactos chicos: métricas,
# muestras generadas y logs.
#
# scripts/remote.sh sync # empuja el código al servidor
# scripts/remote.sh run <config.yaml> [args] # entrena bajo tmux
# scripts/remote.sh logs [corrida] # sigue el log en vivo
# scripts/remote.sh pull <corrida> # trae métricas y muestras
# scripts/remote.sh gpu # estado de la GPU
set -euo pipefail
cd "$(dirname "$0")/.."
[[ -f .env ]] && set -a && source .env && set +a
HOST="${ENLACE_REMOTE_HOST:?definí ENLACE_REMOTE_HOST en .env (ver .env.example)}"
DIR="${ENLACE_REMOTE_DIR:?definí ENLACE_REMOTE_DIR en .env}"
PY="${ENLACE_REMOTE_PYTHON:-$DIR/.venv/bin/python}"
comando="${1:-}"
shift || true
case "$comando" in
sync)
git push
ssh "$HOST" "cd '$DIR' && git pull --ff-only && $PY -m pip install -q -e ."
;;
run)
config="${1:?uso: remote.sh run <config.yaml> [overrides...]}"
shift
# El nombre de la sesión sale del nombre de la config, así que dos corridas
# distintas no se pisan y `logs` sabe a cuál conectarse.
sesion="enlace-$(basename "$config" .yaml)"
# tmux es lo que hace que cortar el SSH no mate el entrenamiento.
ssh -t "$HOST" "cd '$DIR' && tmux new-session -d -s '$sesion' \
\"$PY -m enlace.train.train '$config' $* 2>&1 | tee -a 'runs/$sesion.log'\" \
&& echo 'corriendo en tmux: $sesion'"
;;
logs)
sesion="${1:-}"
if [[ -z "$sesion" ]]; then
ssh "$HOST" "cd '$DIR' && ls -t runs/*.log | head -1 | xargs tail -f"
else
ssh "$HOST" "cd '$DIR' && tail -f 'runs/$sesion.log'"
fi
;;
pull)
corrida="${1:?uso: remote.sh pull <corrida>}"
mkdir -p "runs/$corrida"
# Solo métricas y muestras: los checkpoints se quedan en el servidor.
rsync -av --include='metrics.jsonl' --include='samples.txt' --include='*.yaml' \
--exclude='*' "$HOST:$DIR/runs/$corrida/" "runs/$corrida/"
;;
gpu)
ssh "$HOST" "nvidia-smi"
;;
*)
sed -n '2,14p' "$0"
exit 2
;;
esac
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Corre la suite de tests.
#
# PYTEST_DISABLE_PLUGIN_AUTOLOAD evita que pytest cargue plugins de otros
# entornos presentes en el PYTHONPATH del sistema (ROS, por ejemplo), que
# rompen la colección de tests con errores de importación ajenos al proyecto.
set -euo pipefail
cd "$(dirname "$0")/.."
PYTHON="${PYTHON:-.venv/bin/python}"
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 "$PYTHON" -m pytest "$@"