tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1181Shell 82Rust 62Markdown 521515 Definiciones

SDD 26 §6.7: el motor de inferencia local, y la corrección al propio documento

La tabla del §6 le ponía costo «bajo» a la IA local porque `rimay`/`iniy` parecían alcanzar. Se
miró antes de empezar y no alcanzan: los backends de `pluma-llm` son todos de nube, y
`rimay-verbo-fastembed` descarga onnxruntime (glibc) y el modelo de HuggingFace en el primer
arranque. Queda escrito, porque es lo que evita que alguien vuelva a estimarlo en «bajo».

Y de paso junta dos filas que el plan llevaba separadas: el §6.7 y la mitad semántica del §6.3 no
eran dos problemas — era que el corpus no tenía con qué correr un modelo. Un solo muro.

Queda documentado el hallazgo caro, con las dos líneas de cmake enfrentadas: SOURCE_DATE_EPOCH
—que exportamos para que los builds REPRODUZCAN— apagaba INS_ENB y con él las seis perillas de ISA
de ggml, sellando un motor de inferencia con CERO instrucciones vectoriales que corría igual.
Tabla 0/0/0 vs 42.356/1.298/86, medida sobre artefactos sellados.

Y las dos trampas del arnés que van a volver cuando se pinee el modelo de producción: `/health`
contesta 503 mientras carga, y un GGUF sin tipo de pooling hace que el endpoint compatible con
OpenAI conteste 400 — un error que parece del cliente.

⚠ Escrito también lo que esto NO es: el motor no es la función. Falta el modelo (fuente pineada,
como el perfil de PGO), los verbos del host y quién levanta el servidor. Y `llama-cpp` no se
declara en ningún perfil todavía: una imagen no crece 199 M por una función que aún no existe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqBhowvFe3aiieGCKgvxwa

bcd1cdefaf56110b39fec73fea6942ec14fbc1bd · por Sergio · (hace 1 mes) · padre fb8bbba0b · ver el árbol en este commit

Delta semántico

Ninguna definición cambió: el cambio es de texto, documentación o datos.

1 archivos · +126 −2

diff --git a/docs/26-atuq-envoltorio-gecko.md b/docs/26-atuq-envoltorio-gecko.md
index 767418c2..aff6db46 100644
--- a/docs/26-atuq-envoltorio-gecko.md
+++ b/docs/26-atuq-envoltorio-gecko.md
@@ -493,7 +493,7 @@ La columna «quién más lo tiene» es lo que evita que nos contemos un cuento.
 | 6.4 | **Historial y perfil sobre `qullqa`** | nadie | `qullqa-core`, `qullqa-pozo` | medio |
 | 6.5 | **Foco por `cortafuegos`, no por extensión** ✅ **cadena y navegador; falta quién lo aplica** | nadie (nadie es dueño del navegador *y* del sistema) | `cortafuegos`, `nftables` (nuevo en el corpus) | bajo |
 | 6.6 | **Medios por fuera del navegador** | extensiones sueltas; de fábrica no | `foreign-ytdlp`, `-platform`, `-dlna` | bajo |
-| 6.7 | **IA local en la barra lateral** | Chrome/Edge son nube; Zen no tiene | `rimay`, `iniy` | bajo |
+| 6.7 | **IA local en la barra lateral** ✅ **motor**; falta modelo y barra | Chrome/Edge son nube; Zen no tiene | `llama-cpp` (NUEVO en el corpus) — `rimay`/`iniy` NO servían, ver §6.7 | bajo |
 | 6.8 | **Proxy por contenedor** ✅ **v0.5** | nadie de fábrica | — (API de Firefox) | bajo |
 | 6.9 | **Torrent adentro** | **Vivaldi lo trae; Brave tuvo WebTorrent** | `shared/foreign-torrent` (SDD propio, sobre librqbit) | bajo |
 
@@ -634,7 +634,7 @@ infraestructura que la imagen todavía no lleva:
 | mitad | estado |
 |---|---|
 | **congelar cada página leída y poder encontrarla** | ✅ HECHA — `archive.add` / `archive.search` + la extensión `archivo@atuq.tawasuyu` |
-| **preguntarle al historial en lenguaje natural** | ❌ falta, y ya tiene forma: un motor `rag-motor::RagMotor`, como `willay-rag` |
+| **preguntarle al historial en lenguaje natural** | ❌ falta, pero **el muro cayó el 2026-09-11**: el motor de embeddings ya está en el corpus (§6.7, `llama-cpp` sirve `/v1/embeddings`, medido). Queda pinear el modelo y escribir el `rag-motor::RagMotor`, como `willay-rag` |
 
 Lo segundo no se «aproxima»: `willay-rag` —el motor del centro de eventos, que es el ejemplo a
 copiar— necesita un **daemon de embeddings** (`rimay-verbo`) y un **backend LLM real** (`pluma-llm`),
@@ -838,6 +838,129 @@ puesto. Se comprobó rompiéndolo a propósito.
 una petición del ARRANQUE compite con la inicialización de la extensión. El guardián navega a una
 página que redirige a los tres segundos — que además es lo que hace una persona: seguir un enlace.
 
+### 6.7 El motor de inferencia local — ENTRA AL CORPUS (2026-09-11)
+
+**La primera corrección es al propio §6.** La tabla de arriba le pone costo «bajo» a la IA local
+porque las piezas parecían estar escritas en tawasuyu (`rimay`, `iniy`). Se fue a mirar antes de
+empezar y **no alcanzan**, por dos razones independientes:
+
+| pieza | por qué no corre un modelo acá |
+|---|---|
+| `pluma-llm` | sus backends son `anthropic`, `cohere`, `claude-cli` y `mock`. **Todos de nube o de juguete** |
+| `rimay-verbo-fastembed` | trae `fastembed` con la feature `ort-download-binaries`: **baja onnxruntime precompilado (glibc) y el modelo de HuggingFace en el primer arranque**. En un build hermético y en una distro musl eso no es lento, es imposible |
+
+Y eso además **junta dos pendientes que el plan llevaba separados**: el §6.7 (la barra lateral) y la
+mitad semántica del §6.3 (preguntarle al archivo personal en lenguaje natural) no eran dos problemas.
+Eran uno: **el corpus no tenía con qué correr un modelo.** El muro tiene forma de receta, y una sola
+lo derriba entero — el mismo binario sirve `/v1/chat/completions` (el §6.7) y `/v1/embeddings` (lo
+que `rag-motor` exige para el §6.3).
+
+`recipes/llama-cpp.toml` ⇒ **b10901, estática, 199 M**, 16 binarios sin un solo `NEEDED`.
+`llama-server`, `llama-cli`, y de yapa `llama-quantize`/`llama-bench`/`llama-perplexity`, que son las
+tres herramientas con las que se mide un modelo **antes** de pinearlo. **REPRODUCE** bit a bit
+(`verificar-repro.sh`: 0 no-determinismos).
+
+#### ⚠ La variable que nos da reproducibilidad le apagó el AVX2 al motor, y el build salió 0
+
+Es lo único de esta unidad que no se podía deducir, y por eso va escrito acá con la línea leída.
+
+La receta nació con `-DGGML_NATIVE=OFF`, apoyada en `ggml/CMakeLists.txt:141`:
+
+```cmake
+if (GGML_NATIVE OR NOT GGML_NATIVE_DEFAULT)
+    set(INS_ENB OFF)     # ← con NATIVE=ON, ggml se apoya en -march=native y apaga las perillas
+else()
+    set(INS_ENB ON)      # ← con NATIVE=OFF *debería* encender SSE4.2/AVX/AVX2/BMI2/FMA/F16C
+endif()
+```
+
+Leído así, `GGML_NATIVE=OFF` era exactamente lo que queríamos: nada de `-march=native` (que sería un
+artefacto distinto por máquina — la familia del `MAKE_STAMP` de nftables) y aun así un binario
+vectorizado. **Y es falso en este sandbox**, por una línea 36 renglones más arriba:
+
+```cmake
+if (CMAKE_CROSSCOMPILING OR DEFINED ENV{SOURCE_DATE_EPOCH})
+    set(GGML_NATIVE_DEFAULT OFF)
+```
+
+El sandbox de takana exporta `SOURCE_DATE_EPOCH=1` (`sandbox.rs:453`) **justamente para que los
+builds reproduzcan**. Con eso `GGML_NATIVE_DEFAULT` queda OFF, `NOT GGML_NATIVE_DEFAULT` es
+verdadero, e `INS_ENB` termina en OFF: las seis perillas apagadas. Dicho corto: **la variable que
+existe para que el build reproduzca apagó todas las instrucciones vectoriales del motor de
+inferencia.**
+
+⚠ **Y no falló nada.** El artefacto selló, `llama-cli --version` contestaba, el binario corría. Medido
+sobre el artefacto sellado `b3:e3c62185`, no sobre el log:
+
+| | primer sello (`-DGGML_NATIVE=OFF` a secas) | con las seis declaradas |
+|---|---|---|
+| `%ymm` (AVX2) | **0** | 42.356 |
+| `vfmadd` (FMA) | **0** | 1.298 |
+| `roundps` (SSE4.2) | **0** | 86 |
+
+Un motor de inferencia sin AVX2 no está roto: está varias veces más lento, y **ninguna métrica del
+repo lo dice**. Es la figura de siempre — un ausente falla ruidosamente; un desviado llega hasta el
+final diciendo que todo fue bien. Por eso las seis van declaradas una por una, no se confía en el
+default de nadie, y **el `install` las comprueba en el binario**: es el único punto donde este modo
+de fallar se puede ver.
+
+⚠ Eso fija un **piso declarado: x86-64-v3**. Un CPU sin AVX2 (pre-2013) muere con SIGILL, no con un
+mensaje. Medido antes de elegirlo: `momento`/gioser, el worker `dev.gioser.net` y el objetivo de
+metal (TigerLake) traen los seis flags. Bajar el piso o publicar variantes es
+`docs/plan-variantes-cpu.md`, no esta receta.
+
+De paso, `strip_debug = true`: zig cc emite `debug_info` por defecto y acá son 16 binarios
+**estáticos**, o sea 16 copias de los símbolos de `libllama`+`libggml`+`libc++`. **1,8 G → 199 M**.
+
+#### El guardián, y lo que cada sonda afirma
+
+`scripts/test-llama-cpp.py`, sobre el artefacto **vigente** (resuelto por `takana hash`, nunca por
+`ls store/*` — es la trampa del `gmp` viejo del §6.5), con escotilla `LLAMA_DIR` que grita al usarse:
+
+```
+1. ISA          42.356 %ymm · 1.298 vfmadd · 86 roundps      ← control negativo MEDIDO: 0/0/0
+2. hermético    llama-server y llama-cli: sin NEEDED
+3. inferencia   una pasada REAL con stories260K (1,1 MB, pineado por sha256) ⇒ 51 caracteres
+4. embeddings   vector de 64 dimensiones · mismo texto = mismo vector ·
+                textos distintos = vectores distintos · no son ceros
+```
+
+La sonda 4 es la que le importa al §6.3, y sus tres comprobaciones no son ceremonia: **«¿devuelve un
+vector?» lo pasa un stub de ceros**, y «¿devuelve algo distinto?» lo pasa un generador de ruido. Hace
+falta que el mismo texto dé lo mismo *y* que textos distintos den cosas distintas.
+
+El control negativo es vivo: `--sin-modelo` corre las mismas sondas contra un modelo inexistente y
+**tiene que fallar** — sin eso, un arnés roto («no contestó») se lee igual que un motor que anda.
+Y el censo después de correr: cero `llama-server` sueltos.
+
+⚠ **Dos trampas del arnés que van a volver cuando se pinee el modelo de producción:**
+
+1. **`/health` contesta MIENTRAS carga**, con 503 y `{"status":"loading model"}`. Tomar «contestó»
+   por «listo» manda la primera consulta contra un servidor a medio levantar, y el 503 se lee como
+   «el endpoint no existe». Se espera el `ok`, no el 200.
+2. **`--pooling mean` no es un gusto.** Un GGUF declara su tipo de pooling en los metadatos y los
+   modelos de embedding de verdad (e5, bge, nomic) lo traen; `stories260K` es un LM causal de juguete
+   y no lo trae, así que llama.cpp cae en `none` y el endpoint compatible con OpenAI contesta
+   **`400: Pooling type 'none' is not OAI compatible`** — no un vector vacío, no un 500: un 400 que
+   parece un error del cliente.
+
+#### ⚠ Lo que esto NO es
+
+**Esto es el motor, no la función.** Un motor sin modelo no contesta nada, y `atuq` todavía no tiene
+barra lateral de IA. Falta, y tiene forma:
+
+- **el modelo**, que es **fuente pineada por sha256**, no receta — el mismo patrón que el perfil de
+  PGO de firefox. Son dos decisiones distintas y las dos cuestan disco en la imagen: uno de
+  embeddings para el §6.3 (chico, ~100 MB) y uno de chat para el §6.7. `stories260K` es un modelo
+  REAL pero de juguete: sirve para ejercitar la cadena, no para contestar;
+- **los verbos en el host** (`puriy-costura`): un `ai.ask` y un `archive.search` semántico que hablen
+  con `llama-server`. Hoy `archive.search` es LITERAL y por eso se llama `search`;
+- **quién levanta el servidor.** Misma pregunta que dejó abierta el torrent del §6.9, y ahí ya hay
+  respuesta escrita: un daemon propio, perezoso, con `setsid` para que sobreviva al navegador.
+
+`llama-cpp` **no está declarada en ningún perfil todavía**, y es a propósito: una imagen no debería
+crecer 199 M por una función que aún no existe. Se declara cuando el §6.7 se pague entero.
+
 ### 6.8 Proxy por contenedor — HECHO (v0.5, 2026-09-06)
 
 **Es el primer diferenciador del §6 que se paga entero**, y se pudo pagar ahora porque es el único
@@ -1556,6 +1679,7 @@ siguiente.
 | 8 | **Archivo personal — la mitad de congelar y buscar, HECHA 2026-09-10.** `archive.add`/`archive.search` + la extensión `archivo@atuq.tawasuyu`: el HTML ya ejecutado al CAS, el índice buscable en JSON, y nada de ventanas privadas. Guardián `scripts/test-atuq-archivo.py`, cuyo control negativo además **dice quién** impidió archivar lo privado. **Falta la mitad semántica**: un motor `rag-motor::RagMotor` (como `willay-rag`), que exige daemon de embeddings + LLM real | 6.3 | 5 ✅, 7 ✅ |
 | 9 | **Medios (6.6) ✅ y torrent (6.9) ✅ — 2026-09-10.** El medio lo abre `mpv`; el torrent lo toma `puriy-costura-torrent`, un daemon **propio, configurable y perezoso** que sobrevive al navegador y cosecha al CAS. Medido antes de decidir: la pila de librqbit compila para musl (226 crates), así que la decisión fue «no ahí» y no «no se puede». **Faltan** el foco (6.5) y la IA local (6.7), ésta bloqueada porque el corpus no tiene modelo ni embeddings | 6.5–6.7, 6.9 | 5 ✅ |
 | 10 | **Foco (6.5) — 2026-09-10.** Entra al corpus la cadena `nftables` (libmnl + libnftnl + nft 1.1.6), que el grafo pedía y nadie había puesto, con dos arreglos de fábrica: el sello de tiempo que rompía la reproducción y un bashismo. Medido con root: el cgroup en foco no sale y el de al lado sí. Y del lado del navegador, la extensión `foco` MUESTRA el estado y **no tiene verbo para apagarlo**. **Falta** quién pone a `atuq` en un cgroup (delegación, decisión de arje) y quién aplica la política (root) | 6.5 | 6 ✅ |
+| 11 | **Motor de inferencia local (6.7) — 2026-09-11.** Entra `recipes/llama-cpp.toml` (b10901, estática, 199 M, **REPRODUCE**), que resulta ser **un solo muro para dos pendientes**: el §6.7 y la mitad semántica del §6.3 no eran dos problemas, era que el corpus no tenía con qué correr un modelo (`pluma-llm` sólo tiene backends de nube; `rimay-verbo-fastembed` DESCARGA onnxruntime glibc + el modelo). ⚠ Y dejó medido lo que no se podía deducir: **`SOURCE_DATE_EPOCH` —la variable que nos da reproducibilidad— apagaba las SEIS perillas de ISA de ggml**, y el artefacto sellaba y corría con 0 `%ymm`. Ahora van declaradas y el `install` las comprueba. Guardián `scripts/test-llama-cpp.py` con control negativo vivo. **Faltan** el modelo (fuente pineada, no receta), los verbos del host y quién levanta el servidor | 6.7, y la mitad semántica de 6.3 | 5 ✅ |
 | 9.a | **Proxy por contenedor ✅ v0.5** — contenedores por política + extensión con `proxy.onRequest` | 6.8, y NO dependía de 5: es API de Firefox | — |
 
 Las unidades 2 y 4 son **paralelizables**: la toolchain no toca el chrome y el chrome no toca la