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
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user