From bcd1cdefaf56110b39fec73fea6942ec14fbc1bd Mon Sep 17 00:00:00 2001 From: Sergio Date: Fri, 11 Sep 2026 03:05:20 +0000 Subject: [PATCH] =?UTF-8?q?SDD=2026=20=C2=A76.7:=20el=20motor=20de=20infer?= =?UTF-8?q?encia=20local,=20y=20la=20correcci=C3=B3n=20al=20propio=20docum?= =?UTF-8?q?ento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01GqBhowvFe3aiieGCKgvxwa --- docs/26-atuq-envoltorio-gecko.md | 128 ++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) 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