# 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 ` · `promote `. - **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ó.