gioser

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 815Rust 56Shell 37Markdown 291229 Definiciones

ia: el modelo de embeddings elegido NO servía — Qwen3-Embedding-0.6B en su lugar, y un guardián que lo habría cazado

multilingual-e5-small sellaba, cargaba, contestaba 384 dimensiones y 45 tests en verde. Y con el
modelo de verdad, de punta a punta, el ranking devolvía SIEMPRE la misma página.

Seis pasos descartando hipótesis —batching, posición, nuestro código, la cuantización, la conversión—
hasta que `/tokenize` lo dijo en una línea: «cortafuegos», «minino», «duerme» y «tejado» van todos al
id 100 = `<unk>`. Un vocabulario XLM-RoBERTa por esta ruta deja casi todo en desconocido, y un texto
que es todo `<unk>` embebe igual que cualquier otro. La pista estaba a la vista desde el principio:
`gato~perro` y `gato~cortafuegos` daban el mismo número a CUATRO DECIMALES.

En su lugar, Qwen3-Embedding-0.6B Q8_0, GGUF oficial de Qwen (Apache-2.0): tokeniza español de verdad
(`cort|af|uegos`), acierta 3/3 con márgenes anchos (0,649 contra 0,237), y es de la misma familia que
el modelo de chat. Cuesta 610 MiB en vez de 126: es el precio de que funcione, y sube la cuenta de la
imagen a ~1,85 GiB si se declara.

⚠ Y LA PARTE QUE IMPORTA PARA LA PRÓXIMA VEZ: el guardián del `install` ya no mira sólo el mágico y
el tamaño —«existe» no es «sirve»—. Ahora tokeniza dos frases en español sin palabras en común y
exige que no compartan tokens (con el e5 roto compartían la mitad, todos `<unk>`), y después levanta
el servidor de verdad y exige que un gato se parezca más a un perro que a un cortafuegos. Con esos
dos chequeos el e5 no habría sellado nunca.

El tar del modelo roto se borró del mirror (476 MB) y del disco.

⚠ El build está en la cola del flock detrás de otro agente (pixi, 22 min y contando), así que el
artefacto todavía no está sellado y los guardianes del navegador no corrieron. Lo medido hasta acá:
el camino completo host→motor→índice con el modelo nuevo, 3/3 (`archive.ask` de punta a punta, fuera
de la jaula), más 45 tests en tawasuyu.

c78c5ff91f3a8e8190f124b77be25e9da606704d · por Sergio · (hace 1 mes) · padre 5f2f50200 · ver el árbol en este commit

Delta semántico

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

3 archivos · +147 −49

diff --git a/docs/26-atuq-envoltorio-gecko.md b/docs/26-atuq-envoltorio-gecko.md
index 6eefb9ac..3d69f49e 100644
--- a/docs/26-atuq-envoltorio-gecko.md
+++ b/docs/26-atuq-envoltorio-gecko.md
@@ -716,11 +716,55 @@ siempre la más reciente— pasaría sin haber entendido nada. Y las preguntas *
 con el texto de su página; si las compartieran, la búsqueda literal también acertaría y esto no
 probaría nada semántico.
 
-⚠ **Al cerrar esta entrada el guardián está escrito pero NO ha corrido todavía** contra el artefacto
-sellado: `ia-modelo-embeddings` estaba en la cola del `flock` detrás del build de otro agente. Lo que
-sí está medido es todo lo de abajo del cable —35 tests de `puriy-costura`, 6 del puente, 4 del
-proveedor— y el modelo mismo, probado a mano antes de pinearlo. Esta línea se borra cuando el
-guardián dé verde.
+#### 6.3.quater El modelo que elegimos NO SERVÍA, y el síntoma no se parecía a la causa (2026-09-12)
+
+Esto es la unidad de trabajo más caras de esta tanda y vale escribirla entera, porque la clase de
+fallo se repite: **un modelo de embeddings roto no falla — contesta vectores plausibles y ordena al
+azar.**
+
+La primera elección fue **multilingual-e5-small** (MIT, 118 M, el tamaño ideal, y lo que pedía el
+caso: español). Sellaba, cargaba, contestaba 384 dimensiones, el endpoint funcionaba y los 45 tests
+de la suite estaban en verde. Y la primera medición con el modelo DE VERDAD, de punta a punta, dio
+esto:
+
+```
+«¿dónde se echó a dormir el gato?»      0.9177 gato · 0.8986 red · 0.8965 pan   ✓
+«cómo bloqueo internet a un programa»   0.9337 gato · 0.9112 red · 0.9067 pan   ✗
+«qué lleva el pan»                      0.9233 gato · 0.9066 red · 0.9063 pan   ✗
+```
+
+Gana siempre la misma página. La caza, en el orden en que se hizo —cada paso descartando una
+hipótesis, no confirmándola—:
+
+1. **¿el batching?** No: los puntajes son idénticos pidiendo los pasajes juntos o de a uno;
+2. **¿la posición?** No: cambiando el orden de los pasajes los puntajes no se mueven. (Y esto mató
+   además una sospecha razonable: mi medición «buena» anterior tenía el pasaje correcto en la
+   posición 0, así que podía haber sido un falso positivo. No lo era, pero no se sabía);
+3. **¿nuestro código?** No: los mismos textos a mano contra el servidor dan lo mismo;
+4. **¿la cuantización nuestra?** No: el fp32 sin tocar falla igual;
+5. **¿la conversión de terceros?** No: dos conversos independientes fallan **idéntico** — y ese
+   «idéntico» era la pista que estaba a la vista desde el principio: `gato~perro` y
+   `gato~cortafuegos` daban **el mismo número a cuatro decimales**;
+6. **es el TOKENIZADOR**, y `/tokenize` lo dijo en una línea: «cortafuegos», «minino», «duerme» y
+   «tejado» van todos al **id 100 = `<unk>`**. Un vocabulario XLM-RoBERTa por esta ruta deja casi
+   todo en desconocido, y un texto que es todo `<unk>` embebe igual que cualquier otro.
+
+**Lo que se pineó en su lugar: Qwen3-Embedding-0.6B Q8_0**, el GGUF **oficial** de Qwen
+(Apache-2.0). Tokeniza español de verdad —`cortafuegos` → `cort|af|uegos`—, acierta **3/3** con
+márgenes anchos (0,649 contra 0,237 el segundo) y es de la misma familia que el modelo de chat, que
+ya estaba medido contestando en español. Cuesta **610 MiB**, cinco veces el e5 que no servía: es el
+precio de que funcione, y cambia la cuenta de la imagen (motor + chat + embeddings ≈ 1,85 GiB).
+
+Dos configuraciones que el número decidió, no el gusto: **`--pooling last`** (con `mean` también
+acierta 3/3, pero pegado: 0,574 contra 0,557) y **la instrucción delante de la consulta** (sin ella,
+2/3 — no es decorado).
+
+⚠ **Y la regla que queda, que ahora VIVE en la receta**: el guardián del `install` ya no mira sólo el
+mágico y el tamaño. Tokeniza dos frases en español sin palabras en común y exige que **no compartan
+tokens** (con el e5 roto compartían la mitad, todos `<unk>`), y después levanta el servidor de verdad
+y exige que **un gato se parezca más a un perro que a un cortafuegos**. Con esos dos chequeos, el e5
+no habría sellado nunca — y la caza de seis pasos no habría hecho falta.
+
 
 ### 6.4 `qullqa` con su advertencia puesta
 
diff --git a/recipes/ia-modelo-embeddings.toml b/recipes/ia-modelo-embeddings.toml
index 1b5ac7da..ef0ce177 100644
--- a/recipes/ia-modelo-embeddings.toml
+++ b/recipes/ia-modelo-embeddings.toml
@@ -1,46 +1,59 @@
 # ia-modelo-embeddings — el modelo con el que se le pregunta al archivo personal en lenguaje natural
 # (SDD 26 §6.3, mitad semántica).
 #
-# ══ QUÉ ES, Y POR QUÉ ÉSTE ═════════════════════════════════════════════════════════════════════
-# **multilingual-e5-small**: 384 dimensiones, 118 M de parámetros. Elegido por una razón que se
-# puede medir: **el archivo personal va a estar en español**, y un modelo de embeddings entrenado
-# sólo en inglés buscaría peor justo donde más hay. Licencia MIT (del modelo base, `intfloat`).
+# ══ QUÉ ES ═════════════════════════════════════════════════════════════════════════════════════
+# **Qwen3-Embedding-0.6B, Q8_0 oficial**: 1024 dimensiones, 639.160.320 bytes. Es el GGUF que publica
+# Qwen, no una conversión de terceros — y eso, acá, resultó ser lo que decide si el modelo SIRVE.
 #
-# ══ POR QUÉ LA RECETA CUANTIZA EN VEZ DE PINEAR EL GGUF YA CUANTIZADO ══════════════════════════
-# La fuente pineada es el **fp32 (476 MB)** del único repo que declara licencia (MIT) y la receta lo
-# baja a **Q8_0 (126 MB)** con NUESTRO `llama-quantize` —el del corpus—. Así:
-#   · la procedencia es de quien declara la licencia, no de un repo cualquiera con un q8 subido;
-#   · la imagen se lleva 126 MB y no 476;
-#   · y la cuantización es una transformación DETERMINISTA nuestra, verificable con
-#     `verificar-repro.sh`, en vez de bytes de un tercero que nadie puede reproducir.
+# ══ ⚠ LA HISTORIA IMPORTA, PORQUE EL FALLO NO SE PARECÍA A LA CAUSA ════════════════════════════
+# La primera elección fue **multilingual-e5-small** (MIT, 118 M, el tamaño ideal). Sellaba, cargaba,
+# contestaba 384 dimensiones y el endpoint funcionaba. Y el ranking devolvía **siempre la misma
+# página**, para cualquier pregunta.
 #
-# ⚠ EL MODELO EXIGE PREFIJOS, Y NO ES UN DETALLE DE ESTILO. e5 se entrenó con `query: ` en la
-# consulta y `passage: ` en el documento. Medido con este mismo artefacto contra tres pasajes y una
-# pregunta: el pasaje correcto gana en los dos casos, pero con prefijos la distancia al segundo se
-# abre (0,9371 vs 0,9212 con prefijos; 0,9182 vs 0,8976 sin ellos). Los pone el consumidor
-# (`rimay-verbo-llama`), no esta receta, pero queda escrito acá porque el que pinea el modelo es el
-# que sabe qué convención tiene.
+# Lo que se midió, en este orden:
+#   1. ¿es el batching? No: los puntajes son idénticos pidiendo los pasajes juntos o de a uno.
+#   2. ¿es la posición? No: cambiando el orden de los pasajes, los puntajes no se mueven.
+#   3. ¿es nuestro código? No: los mismos textos a mano contra el servidor dan lo mismo.
+#   4. ¿es la cuantización? No: el fp32 sin tocar falla igual.
+#   5. ¿es la conversión? No: dos conversos independientes fallan **idéntico**.
+#   6. **Es el TOKENIZADOR**, y `/tokenize` lo dijo en una línea: «cortafuegos», «minino», «duerme»
+#      y «tejado» van todos al **id 100 = `<unk>`**. Un vocabulario XLM-RoBERTa por esta ruta deja
+#      casi todo en desconocido, y un texto que es todo `<unk>` embebe igual que cualquier otro.
+#      La huella era visible desde el principio: `gato~perro` y `gato~cortafuegos` daban **el mismo
+#      número a cuatro decimales**.
 #
-# ⚠ Y LOS COSENOS SON ALTOS EN ABSOLUTO (~0,9 entre frases sin relación). Es normal en e5: sus
-# vectores viven en un cono estrecho. Quien ponga un umbral absoluto («>0,8 es relevante») va a
-# aceptar todo; lo que vale es el ORDEN.
+# ⇒ **REGLA QUE SALE DE ESTO Y VIVE EN EL GUARDIÁN DE ABAJO: antes de creerle a un modelo de
+#   embeddings, preguntarle al tokenizador por una frase del idioma que va a leer.** Un modelo que
+#   tokeniza mal no falla: contesta vectores plausibles y ordena al azar.
+#
+# ══ POR QUÉ ÉSTE, Y QUÉ CUESTA ═════════════════════════════════════════════════════════════════
+# · **Apache-2.0** y GGUF **oficial** de Qwen (108 k descargas), no una conversión sin dueño;
+# · **tokeniza español de verdad** — `llama-tokenize` parte «cortafuegos» en `cort|af|uegos`, no en
+#   `<unk>`;
+# · **acierta 3/3** con los tres pasajes y las tres preguntas de prueba, con márgenes anchos
+#   (0,649 contra 0,237 el segundo). Con la consulta pelada, 2/3: la instrucción no es decorado;
+# · misma familia que el modelo de chat, que ya estaba medido contestando en español;
+# · **cuesta 610 MiB** en la imagen, cinco veces el e5 que no servía. Es el precio de que funcione.
+#
+# `--pooling last` (lo pone el consumidor): es el que este modelo usa. Medido: con `mean` también
+# acierta 3/3 pero pegado (0,574 contra 0,557), y sin pooling declarado el endpoint contesta un 400
+# que parece del cliente.
 #
 # ══ EL TARBALL Y LA PROCEDENCIA ════════════════════════════════════════════════════════════════
 # takana extrae toda fuente con `tar` y un `.gguf` pelado no es un tar ⇒ el objeto pineado es un tar
 # que lo envuelve, publicado en el mirror y servido por sha256 (ADR 0013: la URL no entra en
 # `hash_inputs`). Igual que `ia-modelo-chat` y `firefox-pgo-profile`.
 #
-#   upstream  https://huggingface.co/rodion-m/multilingual-e5-small-gguf   (licencia MIT declarada)
-#   fichero   multilingual-e5-small-fp32.gguf
-#   sha256    b3bf20a4ba914b630be1941dc3f5136cc99871d8fa2b402564a3b8752cf010c2   ← del GGUF, no del tar
-#   modelo base  https://huggingface.co/intfloat/multilingual-e5-small (MIT)
+#   upstream  https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF
+#   fichero   Qwen3-Embedding-0.6B-Q8_0.gguf
+#   sha256    06507c7b42688469c4e7298b0a1e16deff06caf291cf0a5b278c308249c3e439   ← del GGUF, no del tar
 name    = "ia-modelo-embeddings"
-version = "multilingual-e5-small-q8_0"
-license = "MIT"
+version = "qwen3-embedding-0.6b-q8_0"
+license = "Apache-2.0"
 
 [source]
 tarball = "https://no-hay-upstream.invalid/takana/ia-modelo-embeddings.tar"
-sha256  = "886098319440f73a79aea2b3ced5a2e17a45f7d65792f6794ffa0c333583e696"
+sha256  = "b7f46f7519c66c65c8ec18c61920d6e6ec318107152b748141993aef4afa2978"
 strip_components = 0
 
 [build]
@@ -52,25 +65,66 @@ compile   = "true"
 install = '''
 set -e
 mkdir -p /out/usr/share/takana/ia
-llama-quantize multilingual-e5-small-fp32.gguf /out/usr/share/takana/ia/embeddings.gguf Q8_0
-
+cp Qwen3-Embedding-0.6B-Q8_0.gguf /out/usr/share/takana/ia/embeddings.gguf
 M=/out/usr/share/takana/ia/embeddings.gguf
 
-# ── GUARDIÁN: QUE SEA UN GGUF CUANTIZADO, NO UN FICHERO ─────────────────────────────────────
-# Dos modos de fallo callados: que `llama-quantize` escriba algo que no es un GGUF, y que escriba el
-# fp32 tal cual (o sea que la cuantización no haya pasado). Se comprueban el mágico y la BANDA de
-# tamaño: un Q8_0 de 118 M de parámetros pesa ~126 MB, así que 80–200 MB deja fuera tanto el
-# truncado como el fp32 de 476 MB.
-head -c 4 "$M" | grep -q GGUF || {
-  echo "!! lo instalado no empieza con el mágico GGUF" >&2
-  head -c 64 "$M" | od -c | head -3 >&2
-  exit 1
-}
+# ── GUARDIÁN 1: QUE SEA UN GGUF ──────────────────────────────────────────────────────────────
+head -c 4 "$M" | grep -q GGUF || { echo "!! lo instalado no empieza con el mágico GGUF" >&2; exit 1; }
 n=$(stat -c%s "$M")
-test "$n" -gt 80000000 || { echo "!! el GGUF pesa $n bytes: está truncado" >&2; exit 1; }
-test "$n" -lt 200000000 || { echo "!! el GGUF pesa $n bytes: parece el fp32 sin cuantizar" >&2; exit 1; }
-echo "guardián: GGUF Q8_0 válido — $n bytes"
+test "$n" -gt 500000000 || { echo "!! el GGUF pesa $n bytes: está truncado" >&2; exit 1; }
+
+# ── GUARDIÁN 2: QUE TOKENICE EL IDIOMA QUE VA A LEER ─────────────────────────────────────────
+# Ésta es la prueba que le faltaba a la receta anterior y que habría ahorrado la caza entera.
+# Un modelo que manda cada palabra a `<unk>` sella, carga y contesta vectores plausibles: el fallo
+# aparece semanas después como «la búsqueda devuelve siempre lo mismo». Dos frases SIN palabras en
+# común tienen que dar conjuntos de tokens distintos; si todo cae en el mismo id de desconocido, se
+# parecen demasiado y esto falla.
+tok() { llama-tokenize --model "$M" --prompt "$1" 2>/dev/null | sed 's/ *\\([0-9]*\\) ->.*/\\1/' | sort -u; }
+tok "el cortafuegos bloquea la salida de una aplicación" > /tmp/t1
+tok "un minino atigrado dormía sobre las tejas del tejado" > /tmp/t2
+c1=$(wc -l < /tmp/t1); c2=$(wc -l < /tmp/t2); comunes=$(comm -12 /tmp/t1 /tmp/t2 | wc -l)
+echo "guardián: tokens distintos $c1 y $c2, comunes $comunes"
+test "$c1" -ge 8 || { echo "!! la primera frase da sólo $c1 tokens distintos: el tokenizador no la entiende" >&2; exit 1; }
+test "$c2" -ge 8 || { echo "!! la segunda frase da sólo $c2 tokens distintos" >&2; exit 1; }
+# Con el e5 roto, las dos frases compartían la mitad de sus ids (todos `<unk>`). Con un tokenizador
+# sano comparten sólo artículos y preposiciones.
+test "$comunes" -le 5 || {
+  echo "!! las dos frases comparten $comunes tokens: huele a que casi todo cae en <unk>" >&2
+  head -20 /tmp/t1 >&2; exit 1
+}
+
+# ── GUARDIÁN 3: QUE EL ESPACIO VECTORIAL DISTINGA ────────────────────────────────────────────
+# «Tokeniza» todavía no es «ordena». Se levanta el servidor de verdad y se le pide que diga que un
+# gato se parece más a un perro que a un cortafuegos. Sin esto, el guardián 2 podría pasar y el
+# modelo seguir siendo inútil para buscar.
+S=/tmp/emb-guardian.sock
+rm -f "$S"
+llama-server --model "$M" --host "$S" --embedding --pooling last --no-warmup > /tmp/emb.log 2>&1 &
+SRV=$!
+listo=0
+i=0
+while [ $i -lt 60 ]; do
+  curl -s --unix-socket "$S" http://localhost/health 2>/dev/null | grep -q '"ok"' && { listo=1; break; }
+  i=$((i+1)); sleep 2
+done
+test "$listo" = "1" || { echo "!! el servidor no levantó el modelo" >&2; tail -20 /tmp/emb.log >&2; kill $SRV 2>/dev/null; exit 1; }
+curl -s --unix-socket "$S" -H 'Content-Type: application/json' \
+  -d '{"input":["gato","perro","cortafuegos"]}' http://localhost/v1/embeddings > /tmp/emb.json
+kill $SRV 2>/dev/null
+# Los tres vectores, uno por línea, y el coseno en awk: el lab no trae python y para esto alcanza.
+tr -d ' \\n' < /tmp/emb.json | sed 's/.*"data"://' | tr '[' '\\n' | sed -n 's/^\\([-0-9.,e]*\\)\\].*/\\1/p' > /tmp/vs
+test "$(wc -l < /tmp/vs)" -ge 3 || { echo "!! no salieron tres vectores" >&2; head -c 300 /tmp/emb.json >&2; exit 1; }
+awk -F, 'NR<=3{n=0;s=0;for(i=1;i<=NF;i++){v[NR,i]=$i;s+=$i*$i;n++};len[NR]=sqrt(s);cols=n}
+END{
+  for(p=2;p<=3;p++){d=0;for(i=1;i<=cols;i++)d+=v[1,i]*v[p,i];cos[p]=d/(len[1]*len[p])}
+  printf "guardián: gato~perro=%.4f  gato~cortafuegos=%.4f\\n", cos[2], cos[3]
+  if (cos[2] <= cos[3] + 0.02) {
+    print "!! el espacio no distingue: un gato se parece a un cortafuegos tanto como a un perro" > "/dev/stderr"
+    exit 1
+  }
+}' /tmp/vs
+echo "guardián: GGUF válido, tokeniza español y el espacio distingue — $n bytes"
 '''
 
 [deps]
-build = ["llama-cpp"]
+build = ["llama-cpp", "curl", "busybox"]
diff --git a/recipes/puriy-costura.toml b/recipes/puriy-costura.toml
index de72a7a5..e285666a 100644
--- a/recipes/puriy-costura.toml
+++ b/recipes/puriy-costura.toml
@@ -43,7 +43,7 @@ license = "MIT OR Apache-2.0"
 
 [source]
 repo   = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git"
-commit = "b4ecffea828a7514e7d397253715c83c1b9b5255"
+commit = "8415e5d12a879026a7518fb2f626bce1bcdac00b"
 # ⚠ SIN ESTO EL BUILD MUERE EN UN SITIO QUE NO NOMBRA LA CAUSA — medido acá el 2026-09-10.
 # tawasuyu COMMITEA su propio `vendor/`: adentro tiene un `smithay` parcheado a mano (tearing/flip
 # async para el compositor), y lo enchufa con `[patch.crates-io] smithay = { path = "vendor/smithay" }`.