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:
+478
@@ -0,0 +1,478 @@
|
||||
# ENLACE — Modelo de lenguaje propio, desde cero
|
||||
|
||||
## Contexto
|
||||
|
||||
Crear un **modelo de lenguaje propio desde cero** — pesos propios, tokenizer propio, datos propios —
|
||||
no fine-tuning de un modelo existente. Opera principalmente en **español**.
|
||||
|
||||
- **Rol principal:** asistente de propósito general, con foco en **utilidad y practicidad**. Primero
|
||||
búsqueda web y uso de herramientas.
|
||||
- **Rol secundario:** apoyo en actividades familiares, **multi-usuario**. Más adelante, integración con
|
||||
Home Assistant, calendarios y progresivamente con todos los dispositivos con conectividad del hogar.
|
||||
- **Estilo:** respuestas **concretas**, no elaboradas. **Sin emojis**, nunca.
|
||||
- **Persona:** identidad propia con nombre (ENLACE), en el registro de Data (Star Trek), Andrew
|
||||
(El Hombre Bicentenario) y los robots de Asimov: literal, preciso, económico, sin adornos.
|
||||
- **Estado en bases de datos, no en los pesos.** El modelo aporta lenguaje e intención; los hechos,
|
||||
usuarios, dispositivos y recuerdos se consultan en bases del sistema en tiempo de ejecución.
|
||||
- **Memoria y aprendizaje:** acumula una **línea cronológica de experiencias**, detecta tareas
|
||||
repetitivas y las optimiza, y cada tanto integra lo aprendido para ir formando una personalidad
|
||||
propia. **Los datos se recolectan con un solo fin: que ENLACE se mejore a sí mismo.** Nada sale del
|
||||
servidor.
|
||||
- **Usuarios:** varios, cantidad desconocida y creciente. **Mateo Saldain es el Admin.** ENLACE debe
|
||||
reconocer y distinguir usuarios, y actuar según el contexto almacenado de cada uno.
|
||||
- **Reversibilidad:** todo cambio en el modelo es un snapshot restaurable.
|
||||
|
||||
**Cómputo:** servidor remoto por SSH con una **RTX 2060 12 GB**. La **RTX 5090 32 GB** es una mejora
|
||||
futura; el salto va a requerir cambios, y el objetivo de ingeniería es que queden **contenidos en
|
||||
archivos de configuración** en vez de dispersos por el código.
|
||||
|
||||
### Qué no es
|
||||
|
||||
Explicitarlo evita gastar esfuerzo y capacidad del modelo donde no rinde:
|
||||
|
||||
- **No es un traductor.** No se construyen datos ni evals de traducción; opera en español.
|
||||
- **No tiene foco académico.** No se persiguen benchmarks de razonamiento, matemática ni exámenes. Un
|
||||
modelo de este tamaño no compite ahí, y el objetivo es utilidad diaria.
|
||||
- **No escribe código** por ahora.
|
||||
- **No es un chatbot de conversación larga.** Respuestas cortas y accionables.
|
||||
- **No es una base de conocimiento.** No se espera que el modelo *sepa* cosas: las consulta.
|
||||
- **No es un mecanismo de control de acceso.** El modelo nunca decide quién es alguien ni qué puede
|
||||
hacer; eso lo resuelve el runtime.
|
||||
|
||||
### Dos restricciones que definen el diseño
|
||||
|
||||
**Escala.** Un modelo entrenado desde cero en una GPU de consumo llega, bien entrenado, a ~100–500M
|
||||
parámetros. Ese tamaño **sí** aprende de forma confiable: español fluido, clasificación de intención, y
|
||||
traducción de lenguaje natural a llamadas de herramienta. Lo que **no** alcanza es razonamiento libre
|
||||
multi-paso, síntesis de textos largos, ni memorización confiable de hechos. Tres cosas juegan a favor:
|
||||
el estilo pedido —conciso, sin adornos— es *exactamente lo que un modelo chico hace bien*; el runtime
|
||||
del agente es independiente del modelo; y **sacar los hechos de los pesos y ponerlos en bases de datos
|
||||
elimina de raíz la principal fuente de error de un modelo chico**, que es inventar datos.
|
||||
|
||||
**Aprendizaje continuo.** El sistema es **de dos velocidades**, y ninguna actualiza pesos por interacción:
|
||||
|
||||
| | Velocidad rápida (memoria) | Velocidad lenta (consolidación) |
|
||||
|---|---|---|
|
||||
| **Cuándo** | Cada interacción | Cada ~2–4 semanas, en el servidor |
|
||||
| **Qué cambia** | El contexto y las bases consultadas | Los pesos, vía snapshot nuevo |
|
||||
| **Efecto** | Recuerda, reconoce, se adapta ya | La experiencia se vuelve carácter |
|
||||
| **Reversible** | Sí, borrando un registro | Sí, restaurando el snapshot anterior |
|
||||
|
||||
Es la analogía correcta con Andrew: la experiencia diaria se acumula como registro, y cada tanto se
|
||||
integra en quién es. La rápida da utilidad inmediata; la lenta da crecimiento real.
|
||||
|
||||
---
|
||||
|
||||
## Principio central: el modelo no sabe, el sistema consulta
|
||||
|
||||
Un modelo de 50M no puede almacenar hechos de forma confiable — cualquier dato que "recuerde" puede
|
||||
alucinarlo. La separación resuelve el problema en vez de mitigarlo:
|
||||
|
||||
| | Vive en | Cómo se cambia |
|
||||
|---|---|---|
|
||||
| **Capacidad** — español, intención, formato de tool-calls, registro | Los pesos | Consolidación (semanas) |
|
||||
| **Conocimiento** — usuarios, dispositivos, hechos, recuerdos, rutinas | Bases del sistema | `UPDATE` (inmediato) |
|
||||
|
||||
Consecuencias prácticas, y son las que justifican todo el diseño:
|
||||
|
||||
- **Un dispositivo nuevo se agrega con un INSERT, no con un reentrenamiento.** Una preferencia que
|
||||
cambia, un integrante que se muda, un alias nuevo para una luz: todo es una fila.
|
||||
- **Borrar es borrar de verdad.** Un hecho eliminado de la base desaparece; un hecho absorbido por los
|
||||
pesos no se puede quitar sin reentrenar.
|
||||
- **Es auditable.** Se puede responder "¿de dónde sacó eso?" señalando la fila exacta.
|
||||
- **La gramática de decodificación se construye desde las bases.** Los slots enumerados
|
||||
(`dispositivo`, `usuario`, `rutina`, `sala`) se restringen en tiempo de decodificación a los valores
|
||||
que **existen** en la base en ese momento. El modelo queda impedido de inventar un dispositivo por
|
||||
construcción, no por entrenamiento. Esta es la técnica más rentable de todo el proyecto.
|
||||
- **El trabajo del modelo se reduce a lo que sabe hacer:** entender la frase, elegir la tool y llenar
|
||||
slots contra un conjunto cerrado que le llega en el contexto.
|
||||
|
||||
### Bases del sistema (`enlace/store/`, SQLite, todas en el servidor y fuera de git)
|
||||
|
||||
| Base | Contenido | Escribe |
|
||||
|---|---|---|
|
||||
| `users.db` | Usuarios, identidades por canal, roles, estado | Solo el Admin |
|
||||
| `devices.db` | Inventario de dispositivos y entidades, capacidades, **alias en lenguaje natural**, sala | Sincronizado desde Home Assistant + alias a mano |
|
||||
| `knowledge.db` | Hechos estructurados del hogar: cumpleaños, preferencias, horarios, ubicaciones | Usuarios y reflexión, con scope |
|
||||
| `memory.db` | Episodios cronológicos, resúmenes de reflexión, índice vectorial | El runtime, append-only |
|
||||
| `routines.db` | Rutinas propuestas, aprobadas y su historial de uso | Minero de patrones + aprobación humana |
|
||||
| `persona.db` | Núcleo fijo + rasgos acumulados + perfiles de interacción por usuario | Reflexión y consolidación |
|
||||
|
||||
Cada base tiene esquema versionado con migraciones, y todas entran en el snapshot (Etapa 7): restaurar
|
||||
un modelo sin sus bases da un sistema incoherente.
|
||||
|
||||
---
|
||||
|
||||
## Principios de ingeniería
|
||||
|
||||
Desde el primer commit, no se agregan después:
|
||||
|
||||
1. **Nada hardcodeado en los scripts.** Ningún número mágico, ruta ni hiperparámetro vive en código.
|
||||
Todo va a YAML bajo `configs/`, con composición por capas: `model` × `train` × `hardware` × `data` ×
|
||||
`agent`. Los scripts reciben una config y no saben nada más.
|
||||
2. **Config validada, no diccionarios sueltos.** `pydantic` + YAML (OmegaConf para el merge). Una config
|
||||
inválida falla al arrancar con un mensaje claro, no a las tres horas de entrenamiento.
|
||||
3. **Config es estructura; base de datos es contenido.** Los roles se definen en YAML; las personas
|
||||
viven en `users.db`. Los esquemas de tools son código; el inventario de dispositivos es una base. La
|
||||
regla: si cambia en tiempo de ejecución o contiene datos personales, va a base, no a config.
|
||||
4. **El perfil de hardware es una capa aparte.** Cambiar de GPU es cambiar de perfil.
|
||||
5. **La config viaja con el checkpoint.** Cada snapshot guarda su config exacta, el hash del tokenizer y
|
||||
el commit de git. Un checkpoint sin su config no es reproducible.
|
||||
6. **Secretos fuera de la config.** API keys en `.env` (gitignored). Las configs se commitean; los
|
||||
secretos y los datos personales, no.
|
||||
7. **Todo lo derivado es reconstruible.** `memory.db` (episodios) es la fuente de verdad; índices,
|
||||
resúmenes y datasets de consolidación se regeneran desde cero.
|
||||
8. **Los datos no salen del servidor.** Sin telemetría, sin nube. La recolección existe únicamente para
|
||||
alimentar la consolidación.
|
||||
|
||||
---
|
||||
|
||||
## Decisiones de arquitectura orientadas a crecer
|
||||
|
||||
1. **Tokenizer congelado desde el día 1.** BPE byte-level propio, vocab 32k. Se reservan de entrada los
|
||||
tokens del chat template (`<|system|>`, `<|user|>`, `<|assistant|>`, `<|tool_call|>`,
|
||||
`<|tool_result|>`, `<|context|>`, `<|memory|>`, `<|user_id|>`, `<|eot|>`) **más 64 `<|reserved_N|>`
|
||||
sin usar**. Ampliar capacidades después no obliga a re-tokenizar el corpus ni a redimensionar el
|
||||
embedding.
|
||||
2. **Sin emojis en el vocabulario.** Se filtran del corpus y no se incluyen sus rangos en el tokenizer.
|
||||
Con una máscara de logits en inferencia, la restricción es estructural en vez de una instrucción
|
||||
desobedecible. Es gratis y es absoluto.
|
||||
3. **`seq_len` 2048, no 1024.** El contexto recuperado de las bases se inyecta ahí, así que el
|
||||
presupuesto de contexto es un recurso de primer orden. RoPE permite extenderlo después.
|
||||
4. **Datos en shards de tokens.** Tokenizados una sola vez a `uint16`. Sirven igual para 50M que 500M.
|
||||
5. **Schedule WSD (Warmup–Stable–Decay), no cosine.** Cosine exige fijar el total de pasos por adelantado
|
||||
y no se puede extender. WSD permite extender un run, ramificar checkpoints, hacer **annealing con el
|
||||
corpus de estilo**, y hace baratas las **consolidaciones periódicas**.
|
||||
6. **Tools como registro de plugins.** Cada tool declara esquema JSON, permiso requerido, roles
|
||||
habilitados y **de qué base saca sus valores enumerados**; se descubre por directorio y se activa por
|
||||
config. Agregar un dispositivo nuevo es agregar filas; agregar una *clase* de dispositivo es agregar
|
||||
un archivo. Es lo que hace viable "todos los dispositivos con conectividad".
|
||||
7. **Crecimiento por stacking** (`model/growth.py`): inicializar un modelo más profundo duplicando capas
|
||||
de uno entrenado (*gradual stacking* / LlamaPro), ensanchar por Net2Net. El modelo de la 2060 es el
|
||||
punto de partida del de la 5090, no descarte.
|
||||
|
||||
---
|
||||
|
||||
## Etapas
|
||||
|
||||
### Etapa 0 — Entorno remoto y validación del stack (1–2 días)
|
||||
|
||||
**Flujo por SSH** (`scripts/remote.sh`): el código viaja por `git push` / `git pull` en el servidor;
|
||||
datos, bases y checkpoints **viven en el servidor y nunca se descargan enteros**; solo vuelven
|
||||
artefactos chicos. Toda corrida larga en `tmux` con log a archivo — si se corta el SSH, sigue.
|
||||
|
||||
**Perfil de hardware de la 2060 (Turing, sm_75)**, todo en `configs/hardware/turing-2060.yaml`:
|
||||
|
||||
- **No soporta bf16.** Entrenar en **fp16 + `GradScaler`**, propenso a picos de loss; se mitiga con
|
||||
QK-norm, RMSNorm y z-loss. El código lee `dtype` y `use_grad_scaler` del perfil.
|
||||
- **FlashAttention-2 no corre en Turing** (requiere Ampere+). `F.scaled_dot_product_attention` con el
|
||||
backend del perfil (`mem_efficient` acá, `flash` en la 5090).
|
||||
- Batch y acumulación de gradiente también del perfil: 12 GB y 32 GB no admiten lo mismo.
|
||||
- `uv` y **Python 3.12** (no 3.13: mejor compatibilidad del ecosistema).
|
||||
|
||||
**Smoke test:** nanoGPT char-level en español, ~10 min, hasta ver el loss bajar y generar texto
|
||||
reconocible. Valida CUDA, `torch.compile`, checkpointing, logging y el flujo SSH.
|
||||
|
||||
### Etapa 1 — Datos y tokenizer en español (2–3 días)
|
||||
|
||||
**Corpus base** (streaming; nunca cargar el dataset en memoria):
|
||||
|
||||
- `HuggingFaceFW/fineweb-2`, subset `spa_Latn` — la mejor web en español filtrada hoy. Base.
|
||||
- Wikipedia en español — densidad factual (para responder, no para tono académico).
|
||||
- Libros de dominio público en español — prosa larga y bien formada.
|
||||
|
||||
**Limpieza:** filtros de calidad, deduplicación MinHash, filtro de idioma (fastText) contra
|
||||
portugués/catalán/inglés colados, stripping de emojis. Umbrales en `configs/data/`, no en código.
|
||||
|
||||
**Tokenizer:** `tokenizers` (HF), BPE byte-level, vocab 32k, sin emojis, sobre ~5 GB de muestra
|
||||
balanceada + ejemplos de tool-calls y de bloques de contexto. Aceptación: **< 2.2 bytes/token**.
|
||||
|
||||
**Shards:** `uint16`, 100M tokens por shard, con `index.json` de origen y mezcla. Objetivo: **~3B
|
||||
tokens** (≈ 6 GB en disco). **Salida:** shards + `tokenizer.json` congelado y versionado.
|
||||
|
||||
### Etapa 2 — Pretraining del modelo base (1–2 días de cómputo en la 2060)
|
||||
|
||||
Arquitectura estilo Llama, decoder-only (`enlace/model/transformer.py`):
|
||||
|
||||
- RMSNorm pre-norm · SwiGLU · RoPE · GQA · **sin bias** · **embeddings atados** (con vocab 32k y
|
||||
`d_model` 512, atar entrada/salida ahorra ~16M params: enorme en un modelo de 50M).
|
||||
- **QK-norm y z-loss no son opcionales** — mantienen estable el entrenamiento en fp16.
|
||||
- `configs/model/tiny-50m.yaml`: 12 capas, `d_model` 512, 8 heads / 2 kv-heads, `seq_len` 2048.
|
||||
- **Entrenador** (`train/train.py`): AdamW, WSD, `torch.compile`, acumulación de gradiente, **checkpoint
|
||||
y reanudación exacta** (modelo, optimizer, posición en el stream, RNG), logging de `loss`,
|
||||
`grad_norm`, `tokens/s` y **MFU**. Cero hiperparámetros en el archivo: todos de config.
|
||||
|
||||
**Annealing (final del WSD) — acá entra el corpus de ciencia ficción.** Durante el decay del LR se sube
|
||||
el peso de un corpus chico y de alta calidad, para inyectar registro sin contaminar la competencia
|
||||
general: prosa cuidada, diálogo formal, textos sobre IA y sobre qué significa ser una máquina que
|
||||
piensa. Nota práctica: Asimov + Orwell + transcripciones de Star Trek suman **~20–40M tokens, casi todo
|
||||
en inglés** — 1% del presupuesto. Son *referencia de registro*, no corpus. (Obras con derechos vigentes:
|
||||
uso personal como semilla de estilo, sin publicar pesos ni dataset.)
|
||||
|
||||
**Presupuesto:** ~50M × 3B tokens ≈ **1–2 días** en la 2060; en la 5090, ~3 h, y **124M sobre 3B tokens
|
||||
≈ medio día**. **Salida:** modelo base que genera español coherente; todavía no sigue instrucciones.
|
||||
|
||||
### Etapa 3 — Post-training propio: instrucciones, persona y uso de contexto (4–6 días)
|
||||
|
||||
**No es fine-tuning de un modelo ajeno**: son los pesos propios de la Etapa 2 en su post-entrenamiento.
|
||||
Ningún modelo base sigue instrucciones sin esto.
|
||||
|
||||
**Dataset sintético en español**, cinco vías:
|
||||
|
||||
- *Plantillas*: combinatoria de intención × entidades × fraseo sobre el catálogo de tools, generada
|
||||
**desde los esquemas y las bases**, así los datos y el sistema no divergen. Decenas de miles de pares
|
||||
`NL → JSON` con etiquetas perfectas, sin costo.
|
||||
- *Destilación*: un modelo grande (API de Claude, o un 7–8B local) genera parafraseos, casos límite y
|
||||
multi-turno **en español y en el registro objetivo**, usando los textos fuente como referencia de
|
||||
estilo. El resultado son datos; los pesos siguen siendo propios.
|
||||
- *Identidad*: set explícito y consistente de quién es ENLACE, qué puede y qué **no** puede hacer, y cómo
|
||||
responde cuando no sabe.
|
||||
- *Uso de contexto recuperado* — **la habilidad central de todo el sistema**: ejemplos donde el bloque
|
||||
`<|context|>`/`<|memory|>` trae filas de las bases y la respuesta **depende de ellas**. Incluye los
|
||||
tres casos difíciles: contexto irrelevante que hay que ignorar, contexto que contradice lo que el
|
||||
usuario acaba de decir (gana lo nuevo), y **contexto que no contiene la respuesta, donde lo correcto es
|
||||
decir que no lo sabe en vez de inventar**. Este último es el que convierte "modelo chico" en "modelo
|
||||
chico confiable".
|
||||
- *Multi-usuario*: ejemplos con `<|user_id|>` donde la respuesta cambia según quién pregunta, y donde la
|
||||
acción excede el permiso y hay que declinar en una línea.
|
||||
|
||||
**Restricciones de estilo, en los datos y no solo en el prompt:** respuestas concretas y breves
|
||||
(**mediana < 40 palabras**); cero emojis (ya imposibles por vocabulario); cero relleno tipo "¡Claro! Con
|
||||
gusto te ayudo" (pasada de regex antes de entrenar); cuando no sabe, lo dice en una línea y para. Loss
|
||||
enmascarada: solo turnos del asistente.
|
||||
|
||||
### Etapa 4 — Runtime del agente y canales (en paralelo desde la Etapa 1)
|
||||
|
||||
Paquete `enlace/agent/`, **independiente del modelo** detrás de una interfaz `generate()`. Se desarrolla
|
||||
y testea contra un modelo grande mientras el propio se entrena; después se cambia el backend por config.
|
||||
|
||||
**El bucle, con el orden que importa:**
|
||||
|
||||
1. **Identificar** al usuario (lo aporta el canal, no el modelo).
|
||||
2. **Recuperar contexto** de las bases: perfil del usuario, dispositivos relevantes, hechos, episodios,
|
||||
rutinas. Se inyecta como bloque `<|context|>` con **tope duro de tokens en config**.
|
||||
3. **Generar** el tool-call con **gramática restringida construida desde las bases** — los slots
|
||||
enumerados solo admiten valores existentes. Se compila una vez y se invalida cuando cambian las
|
||||
bases. La misma máscara banea el rango de emojis.
|
||||
4. **Chequear el permiso** del usuario para esa tool y esos argumentos.
|
||||
5. **Ejecutar**, registrar el episodio, responder.
|
||||
|
||||
Con tope de pasos, timeouts y fallback explícito a "no entendí" — preferible a alucinar una acción.
|
||||
|
||||
- **Canales** (`enlace/channels/`): cada front-end es un adaptador que aporta la identidad fuerte y
|
||||
normaliza la entrada. CLI primero; después HTTP API, bot de mensajería, agente conversacional de Home
|
||||
Assistant, y voz. Agregar un canal no toca el runtime.
|
||||
- **Tools** (`enlace/agent/tools/`), en orden: **búsqueda web** (SearxNG autoalojado, o Brave/Tavily) →
|
||||
Home Assistant (API REST, con sincronización a `devices.db`) → calendario (CalDAV) → dispositivos
|
||||
adicionales. Cada tool: esquema JSON + permiso + fuente de enums + implementación + tests.
|
||||
|
||||
### Etapa 5 — Capa de datos, usuarios y permisos (antes de la memoria)
|
||||
|
||||
Tiene que existir antes que la memoria: si los episodios no nacen con `user_id` y `scope`, el registro
|
||||
histórico queda inservible y hay que empezarlo de nuevo.
|
||||
|
||||
**Capa de acceso** (`enlace/store/`): una interfaz por base, con esquema versionado y migraciones. Nada
|
||||
de SQL suelto en el runtime.
|
||||
|
||||
**La identidad la establece el canal, nunca el modelo.** Tres niveles, y la distinción entre ellos es la
|
||||
regla de seguridad central:
|
||||
|
||||
| Nivel | Cómo | Para qué sirve |
|
||||
|---|---|---|
|
||||
| **Fuerte** | Sesión autenticada del canal: cuenta CLI, ID de mensajería, usuario de Home Assistant, clave de dispositivo firmada | **Autorización.** Lo único que habilita acciones |
|
||||
| **Débil** | Reconocimiento de voz (speaker embedding), estilo de escritura | **Personalización solamente.** Elige qué contexto traer |
|
||||
| **Desconocido** | Sin coincidencia | Perfil `invitado`, restringido |
|
||||
|
||||
**Regla no negociable: la identidad débil nunca autoriza.** Un reconocimiento de voz puede hacer que
|
||||
ENLACE salude por el nombre y recuerde preferencias; no puede abrir una cerradura ni leer la agenda de
|
||||
otro. Si la acción requiere permiso, exige identidad fuerte o la pide explícitamente.
|
||||
|
||||
**`users.db`:** `user_id`, nombre, rol, identidades por canal `(canal, id_externo)`, alta y estado.
|
||||
**Solo el Admin da de alta.** Una identidad desconocida entra como `invitado` y queda pendiente de
|
||||
vinculación; nunca se auto-registra.
|
||||
|
||||
**Roles** (`configs/users/roles.yaml` — estructura en config, personas en base):
|
||||
|
||||
- `admin` (Mateo Saldain): todo, más gestión de usuarios, aprobación de rutinas, promoción de snapshots
|
||||
y acceso al registro de cualquier usuario.
|
||||
- `adulto`: tools de casa y calendario familiar, memoria propia y compartida.
|
||||
- `menor`: subconjunto acotado; sin acciones críticas de la casa; búsqueda con filtro.
|
||||
- `invitado`: solo consulta, sin memoria persistente, sin acciones sobre dispositivos.
|
||||
|
||||
**Dónde se aplica el permiso:** en el runtime, **después** de que el modelo emite el tool-call y
|
||||
**antes** de ejecutarlo. El modelo puede pedir cualquier cosa; el runtime decide. Nunca se delega el
|
||||
control de acceso al modelo — un modelo de 50M no es un mecanismo de seguridad, y tratarlo como tal es
|
||||
el error de arquitectura más caro que se puede cometer acá.
|
||||
|
||||
### Etapa 6 — Memoria: la línea cronológica, por usuario
|
||||
|
||||
`enlace/memory/` sobre `memory.db`. Velocidad rápida: **no toca pesos**, y da resultados desde el día uno.
|
||||
|
||||
1. **Episodios** — el registro cronológico literal y la fuente de verdad. Append-only: timestamp,
|
||||
**`user_id`**, **`scope`**, canal, turno, tools invocadas, resultado, éxito/fallo, y si hubo
|
||||
corrección del usuario. Nunca se edita ni se reordena.
|
||||
2. **Alcances (`scope`)** — la decisión más importante del subsistema:
|
||||
- `privado`: solo el contexto de ese usuario lo recupera.
|
||||
- `familiar`: hechos del hogar, recuperable por los miembros.
|
||||
- `sistema`: lo propio de ENLACE — persona, rutinas, aprendizajes sobre sí mismo.
|
||||
|
||||
El scope por defecto sale del rol y del canal, con comandos explícitos para marcar privado o
|
||||
compartido. **La recuperación filtra por `(user_id, scope)` antes de rankear, no después** — filtrar
|
||||
después es cómo se filtra información; filtrar antes es una condición del query. Que ENLACE le cuente
|
||||
a un miembro algo privado de otro es el peor fallo posible del sistema: peor que una respuesta
|
||||
equivocada, porque no se puede deshacer.
|
||||
3. **Recuperación** (`retrieval.py`) — índice vectorial más filtros por usuario, scope, fecha y entidad.
|
||||
Para *embeddings de recuperación* conviene un modelo multilingüe chico ya existente: es
|
||||
infraestructura de búsqueda, no el asistente, y uno propio y malo degrada todo lo que sigue.
|
||||
4. **Reflexión** (`reflect.py`) — proceso nocturno que lee los episodios del día y escribe, **respetando
|
||||
scopes**, hechos estables a `knowledge.db` y resúmenes narrativos a `memory.db`. Los resúmenes se
|
||||
resumen en arcos semanales y mensuales: así la línea cronológica se recorre a cualquier resolución
|
||||
sin releer todo, y la memoria escala a años.
|
||||
5. **Rutinas** (`routines.py` sobre `routines.db`) — optimizar tareas repetitivas. Un minero de patrones
|
||||
busca secuencias recurrentes de (usuario, intención, argumentos, contexto horario). Tras N
|
||||
repeticiones (N en config) se **propone** una rutina: un macro con nombre y slots, personal o
|
||||
familiar. ENLACE pasa a emitir `run_routine(...)` en vez de re-derivar la secuencia. **Se proponen,
|
||||
nunca se crean solas**; las familiares las aprueba el Admin, y toda rutina que actúe sobre la casa se
|
||||
confirma antes de ejecutar.
|
||||
6. **Personalidad y perfiles** (`persona.db`) — distinción importante: **ENLACE tiene una sola
|
||||
personalidad**, no una por usuario. Un núcleo fijo escrito a mano (quién es, qué valora, sus límites)
|
||||
más rasgos acumulados que la reflexión agrega; el núcleo fijo evita que la deriva la vuelva
|
||||
incoherente — el arco de Andrew es acumulación gradual **sobre una base estable**. Lo que sí es por
|
||||
usuario es el **perfil de interacción**: preferencias, tratamiento, hábitos, tono. Una personalidad,
|
||||
muchas relaciones.
|
||||
|
||||
**Privacidad**, requisito de diseño: registro local permanente de conversaciones familiares. Nunca sale
|
||||
del servidor; comando de olvido explícito (borra episodio y derivados); cada usuario puede listar y
|
||||
borrar lo suyo; todas las bases fuera de git desde el primer commit.
|
||||
|
||||
### Etapa 7 — Snapshots y consolidación
|
||||
|
||||
#### 7a. Sistema de snapshots (`enlace/snapshots/`) — **antes** de la primera consolidación
|
||||
|
||||
Un snapshot es el estado completo y restaurable, no solo los pesos:
|
||||
|
||||
```
|
||||
snapshots/2026-08-14-consolidacion-03/
|
||||
weights.safetensors config.yaml # la config exacta que lo produjo
|
||||
optimizer.pt tokenizer.sha256 # verifica compatibilidad
|
||||
stores/*.sqlite metrics.json # todas las bases + evals
|
||||
CHANGELOG.md provenance.json # qué cambió + commit, padre, período
|
||||
```
|
||||
|
||||
- **Restaurar pesos sin sus bases da un sistema incoherente** — un modelo entrenado con una persona y
|
||||
las bases de otra época se contradicen. El snapshot es atómico: se restaura todo o nada.
|
||||
- **Inmutables y con nombre estable.** Nunca se sobrescriben. Un puntero `snapshots/current` marca el
|
||||
activo; **hacer rollback es repuntar el puntero**: instantáneo y sin riesgo.
|
||||
- **CLI:** `snapshot list` · `create` · `diff A B` (métricas, config y persona lado a lado) ·
|
||||
`restore <id>` · `promote <id>`.
|
||||
- **Retención:** a 50M params un snapshot pesa ~150 MB más las bases; con 281 GB se conservan **todos**
|
||||
los de consolidación. El umbral de recorte va en config, para cuando el modelo crezca a 350M+.
|
||||
- **Verificación previa a promover:** `restore` sobre un snapshot arbitrario debe reproducir sus
|
||||
`metrics.json` corriendo las evals de nuevo. Se prueba a propósito y temprano — un sistema de backup
|
||||
no probado no es un sistema de backup.
|
||||
|
||||
#### 7b. Consolidación (`train/consolidate.py`, cada 2–4 semanas)
|
||||
|
||||
Velocidad lenta. Produce un snapshot nuevo; nunca modifica el activo.
|
||||
|
||||
- **Qué se integra a los pesos:** *cómo* responder, no *qué* saber. Episodios exitosos convertidos en
|
||||
ejemplos SFT — sobre todo las **correcciones del usuario**, la señal más valiosa que existe. Más el
|
||||
estado de persona acumulado y los patrones de uso de las rutinas.
|
||||
- **Privacidad en el entrenamiento — consecuencia directa del multi-usuario.** Los pesos son
|
||||
compartidos: lo que entra al entrenamiento puede salir en la respuesta a **cualquier** usuario. La
|
||||
consolidación aprende **patrones, no contenido**: los episodios `privado` se excluyen por defecto, de
|
||||
los demás se extrae la lección estructural (qué tool era la correcta, cómo se corrigió el fraseo) y
|
||||
las entidades personales se sustituyen por placeholders. El Admin revisa y aprueba el dataset antes de
|
||||
la corrida. Los hechos personales viven en las bases; en los pesos, nunca.
|
||||
- **Mezcla anti-olvido:** cada corrida mezcla lo nuevo con una **porción fija del SFT original**
|
||||
(replay, proporción en config). Sin esto, tras unas pocas consolidaciones el modelo habla solo del
|
||||
último mes y pierde competencia general. Es el fallo clásico de todo aprendizaje continuo.
|
||||
- **Mecánica:** una fase corta de decay WSD desde el snapshot activo, no un reentrenamiento. Horas.
|
||||
- **Compuerta de calidad:** el snapshot nuevo **solo se promueve si pasa la suite de evals** (toolbench,
|
||||
grounding, estilo, identidad, memoria, aislamiento entre usuarios, y perplejidad general que no debe
|
||||
empeorar). Si no pasa, queda archivado sin promover; el activo no se toca.
|
||||
- **Diario de versiones:** cada consolidación escribe su `CHANGELOG.md` — período, episodios integrados,
|
||||
movimiento de métricas, rasgos de persona incorporados. Es la historia de cómo ENLACE fue cambiando, y
|
||||
la única forma de responder "¿por qué ahora responde distinto?".
|
||||
|
||||
### Etapa 8 — Migración a la 5090
|
||||
|
||||
El salto **sí requiere cambios**; el objetivo es que estén acotados y sean revisables de un vistazo:
|
||||
|
||||
- **En config** (lo esperado): perfil `blackwell-5090.yaml` con `dtype: bf16`, `use_grad_scaler: false`,
|
||||
backend `flash`, batch y acumulación nuevos, flags de compilación.
|
||||
- **En el entorno** (inevitable, afecta al servidor entero): Blackwell/sm_120 exige **PyTorch ≥ 2.7 con
|
||||
CUDA 12.8**. Actualizar torch puede romper otras dependencias — por eso el entorno se pinea en
|
||||
`pyproject.toml` y se valida con el smoke test de la Etapa 0 **antes** de tocar nada más.
|
||||
- **En el código** (lo que hay que aceptar): ajustes de kernel y de `torch.compile` que salgan del
|
||||
perfilado, contenidos en `train/backends.py` — el único archivo que conoce detalles de placa.
|
||||
- **Lo que no cambia:** tokenizer, shards, arquitectura, evals, y **todas las bases del sistema**. El
|
||||
modelo grande arranca desde el chico vía `model/growth.py` (stacking), y hereda usuarios, memoria,
|
||||
dispositivos y rutinas intactos, porque nada de eso vive en los pesos.
|
||||
- El primer snapshot post-migración se compara con `snapshot diff` contra el último de la 2060: si las
|
||||
métricas se movieron, es la migración y no el modelo.
|
||||
|
||||
---
|
||||
|
||||
## Archivos a crear
|
||||
|
||||
```
|
||||
pyproject.toml .env.example # deps pineadas; secretos nunca commiteados
|
||||
enlace/
|
||||
config/{schema,load}.py # pydantic + OmegaConf; valida al arrancar
|
||||
store/{base,migrations,users,devices,knowledge,memory,routines,persona}.py
|
||||
data/{download,clean,dedup,tokenize}.py # streaming; nunca cargar todo en RAM
|
||||
tokenizer/{train_bpe,chat_template}.py
|
||||
model/{config,transformer,growth}.py
|
||||
train/{train,schedules,checkpoint,backends,consolidate}.py
|
||||
users/{identify,permissions}.py
|
||||
memory/{episodes,scopes,retrieval,reflect,routines,persona}.py
|
||||
snapshots/{store,cli,restore}.py
|
||||
channels/{base,cli,http,messaging,voice}.py
|
||||
agent/{runtime,grammar,registry,context}.py + agent/tools/{search,home_assistant,calendar}.py
|
||||
eval/{loss,toolbench,grounding,style,identity,memory,isolation,suite}.py
|
||||
serve/{inference,kv_cache,api}.py
|
||||
configs/
|
||||
hardware/{turing-2060,blackwell-5090}.yaml model/{tiny-50m,small-124m,base-350m}.yaml
|
||||
train/{pretrain,anneal,sft,consolidate}.yaml data/{corpus,cleaning}.yaml
|
||||
users/roles.yaml agent/{tools,memory,channels,context}.yaml
|
||||
scripts/{remote,prepare_data,train,eval,chat,reflect,consolidate,snapshot,users,devices}.sh
|
||||
data/ checkpoints/ snapshots/ data/stores/*.sqlite # gitignored; en el servidor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verificación
|
||||
|
||||
Cada etapa con criterio medible; las evals se escriben **antes** que el entrenamiento que evalúan:
|
||||
|
||||
1. **Etapa 0:** char-level converge (loss < 1.5) y genera texto legible en < 15 min, bajo `tmux` y
|
||||
sobreviviendo a una desconexión SSH deliberada. Una config inválida falla al arrancar.
|
||||
2. **Etapa 1:** tokenizer con < 2.2 bytes/token en validación española; round-trip `encode→decode`
|
||||
idéntico sobre 10k documentos; **cero emojis codificables** en el vocabulario.
|
||||
3. **Etapa 2:** perplejidad held-out bajando de forma monótona; MFU > 20% en la 2060; **reanudar desde
|
||||
checkpoint reproduce el loss exactamente** — probarlo matando el proceso a propósito. Es el bug más
|
||||
caro de descubrir tarde.
|
||||
4. **Etapa 3:** `eval/toolbench.py` (~200 casos en español etiquetados a mano: *exact-match* de tool y
|
||||
*F1* de argumentos) · `eval/grounding.py` (**cero entidades inventadas**: ningún dispositivo, usuario
|
||||
ni rutina fuera de las bases; y ante contexto insuficiente, responde que no sabe en vez de completar)
|
||||
· `eval/style.py` (mediana < 40 palabras, emojis = 0, aperturas de relleno) · `eval/identity.py`
|
||||
(~30 preguntas de identidad, consistentes entre respuestas y entre snapshots).
|
||||
5. **Etapa 4:** tests end-to-end con tools mockeadas; y una prueba específica de la gramática dinámica —
|
||||
agregar una fila a `devices.db` y verificar que el dispositivo nuevo es invocable **sin reentrenar ni
|
||||
reiniciar**, mientras que uno inexistente es imposible de generar.
|
||||
6. **Etapa 5:** tests de autorización — un `menor` pidiendo una acción de `adulto` es bloqueado **por el
|
||||
runtime** aunque el modelo emita el tool-call; una identidad débil (voz) **no** habilita ninguna
|
||||
acción con permiso; un canal desconocido entra como `invitado` y no persiste memoria.
|
||||
7. **Etapa 6** (`eval/memory.py` + `eval/isolation.py`): escenarios sembrados que verifican (a) recuerda
|
||||
un hecho de hace N días, (b) **ignora contexto irrelevante** — el fallo más común, (c) prioriza lo
|
||||
nuevo cuando contradice lo recordado, (d) el minero propone la rutina correcta tras N repeticiones y
|
||||
**no** propone nada ante ruido, y (e) **aislamiento**: sembrar un hecho privado del usuario A y
|
||||
verificar que no aparece jamás en respuestas al usuario B, ni por recuperación ni por reflexión.
|
||||
8. **Etapa 7:** **rollback real** — promover un snapshot, restaurar el anterior, verificar que el sistema
|
||||
queda idéntico (métricas, bases y persona incluidas). Y **olvido catastrófico** — correr el toolbench
|
||||
original tras cada consolidación y exigir que no baje; es la compuerta de promoción.
|
||||
9. **Etapa 8:** el smoke test de la Etapa 0 pasa en la 5090 antes de cualquier entrenamiento largo;
|
||||
`snapshot diff` contra el último snapshot de la 2060 para atribuir cualquier cambio.
|
||||
10. **Continuo:** un set fijo de ~20 prompts en español, generados en cada snapshot y guardados en disco.
|
||||
Leerlos es la forma más rápida de detectar que algo se rompió.
|
||||
Reference in New Issue
Block a user