atuq §6.7: entra el motor de inferencia local — y SOURCE_DATE_EPOCH le había apagado el AVX2

La IA local de la barra lateral (§6.7) y la mitad semántica del archivo personal (§6.3) figuraban
como dos pendientes distintos. Son uno: el corpus no tenía con qué correr un modelo. `pluma-llm`
sólo trae backends de NUBE y `rimay-verbo-fastembed` DESCARGA onnxruntime (glibc) y el modelo de
HuggingFace en el primer arranque. `recipes/llama-cpp.toml` (b10901, estática, 199 M) derriba el
muro entero: el mismo binario sirve `/v1/chat/completions` y `/v1/embeddings`.

⚠ Lo que este commit deja medido, y es lo que no se podía deducir: el PRIMER sello llevaba sólo
`-DGGML_NATIVE=OFF` —leído en `ggml/CMakeLists.txt:141`, que con NATIVE=OFF debería ENCENDER las
perillas explícitas de ISA— y salió con CERO instrucciones vectoriales. La causa está 36 líneas
más arriba: `if (CMAKE_CROSSCOMPILING OR DEFINED ENV{SOURCE_DATE_EPOCH})` apaga
`GGML_NATIVE_DEFAULT`, y el sandbox de takana exporta `SOURCE_DATE_EPOCH=1` (`sandbox.rs:453`)
justamente para que los builds REPRODUZCAN. O sea: la variable que nos da reproducibilidad apagaba
todas las instrucciones vectoriales del motor de inferencia — y no falló nada. El artefacto selló,
`--version` contestaba, y el binario era x86-64 pelado: 0 `%ymm`, 0 `vfmadd`, 0 `roundps` en
1.634.770 líneas de `objdump -d`. Ahora las seis van declaradas una por una y el `install` LAS
COMPRUEBA en el binario: 42.356 `%ymm` / 1.298 `vfmadd` / 86 `roundps`.

`strip_debug = true` porque zig cc emite debug_info por defecto y son 16 binarios estáticos:
1,8 G → 199 M.

Guardián `scripts/test-llama-cpp.py`, sobre el artefacto VIGENTE (resuelto por `takana hash`, no
por `ls store/*`) y con control negativo vivo (`--sin-modelo`, que TIENE que fallar): ISA,
hermético (0 NEEDED), una pasada de inferencia REAL con `stories260K` pineado por sha256, y el
endpoint de embeddings — vector de verdad, el mismo texto da el mismo vector, dos textos distintos
dan vectores distintos, y no son ceros. Y `verificar-repro.sh`: REPRODUCE, 0 no-determinismos.

⚠ Esto es el MOTOR, no la función. Un motor sin modelo no contesta nada: el modelo de producción
es fuente pineada aparte, como el perfil de PGO de firefox, y es su propia unidad de trabajo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GqBhowvFe3aiieGCKgvxwa
This commit is contained in:
Sergio
2026-09-11 03:03:31 +00:00
co-authored by Claude Opus 5
parent 9bbd2e42c9
commit 4190c7b43e
2 changed files with 429 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
# llama.cpp b10901 — el MOTOR DE INFERENCIA LOCAL del corpus. Es la pieza que destraba el §6.7 del
# SDD 26 (IA local en la barra lateral de `atuq`) y la mitad semántica del §6.3 (preguntarle al
# archivo personal en lenguaje natural).
#
# ══ POR QUÉ ESTA RECETA, Y NO «ENCHUFAR LO QUE YA HAY» ═════════════════════════════════════════
# El §6.7 figuraba como costo «bajo» porque las piezas parecían estar escritas en tawasuyu. Se fue
# a mirar y NO alcanzan, por dos razones independientes:
# 1. `pluma-llm` tiene backends `anthropic`, `cohere`, `claude-cli` y `mock` — TODOS de nube o de
# juguete. Ninguno corre un modelo en la máquina;
# 2. `rimay-verbo-fastembed` —el daemon de embeddings que `rag-motor` exige— 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.
# O sea que los dos pendientes comparten UN muro y el muro tiene forma de receta. llama.cpp lo
# derriba entero: el mismo binario sirve `/v1/chat/completions` (el LLM del §6.7) y `/v1/embeddings`
# (lo que `rag-motor` necesita para la mitad semántica del §6.3), sin ONNX, sin Python y sin red.
#
# ⚠ ESTA RECETA ES EL MOTOR, NO LA FUNCIÓN. Un motor sin modelo no responde nada. El modelo es una
# FUENTE PINEADA aparte —igual que el perfil de PGO de firefox—, y es su propia unidad de trabajo.
# Nadie debería leer «IA local» y esperar que esto conteste algo todavía.
#
# ══ LA VERSIÓN ES UN COMMIT, NO UNA SERIE ═════════════════════════════════════════════════════
# llama.cpp no publica versiones semánticas: sus releases son `bNNNN`, una por commit de `master`
# (b10901 = 2026-09-10). El `LLAMA_VERSION` interno dice `0.4.0-dev` y no identifica nada. Se pinea
# el tag, que es lo único que nombra un árbol concreto, y subirlo es su propia unidad de trabajo.
#
# ══ LAS CUATRO PERILLAS QUE NO SON GUSTO ══════════════════════════════════════════════════════
#
# 1. **`-DLLAMA_USE_PREBUILT_UI=OFF` — viene ON DE FÁBRICA Y DESCARGA EN TIEMPO DE BUILD.**
# `tools/ui/CMakeLists.txt` cuelga un `add_custom_target` que corre `scripts/ui-assets.cmake`, y
# ése hace `file(DOWNLOAD …)` contra un bucket de HuggingFace para hornear la web UI dentro de
# `llama-server`. Leído en la fuente del tarball pineado, no supuesto. Hoy el sandbox no tiene
# red y el guión sólo avisa y sigue — pero apagarlo por AUSENCIA DE RED es exactamente la
# protección que funciona por accidente: el día que el sandbox tenga red, entraría un blob que no
# está en `hash_inputs` y el artefacto dejaría de reproducir sin que nadie tocara la receta. Se
# apaga por declaración. La web UI no se pierde: no la queríamos — acá el cliente es el host de
# native messaging del §7, que habla HTTP, no un navegador mirando el puerto.
#
# 2. **LAS SEIS PERILLAS DE ISA VAN UNA POR UNA, Y LA RAZÓN SE MIDIÓ ACÁ.**
# El primer sello de esta receta llevaba sólo `-DGGML_NATIVE=OFF`, apoyado en leer
# `ggml/CMakeLists.txt:141` — `if (GGML_NATIVE OR NOT GGML_NATIVE_DEFAULT) set(INS_ENB OFF)`—,
# o sea: con NATIVE=OFF, ggml debería ENCENDER las perillas explícitas (SSE4.2/AVX/AVX2/BMI2/
# FMA/F16C) en vez de apoyarse en `-march=native`. **Y es falso en ESTE sandbox**, por una línea
# 50 renglones más arriba (`ggml/CMakeLists.txt:105`):
#
# 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`) — que es lo que nos da
# binarios reproducibles. Con eso `GGML_NATIVE_DEFAULT` queda OFF, `NOT GGML_NATIVE_DEFAULT` es
# verdadero, e `INS_ENB` termina en **OFF**: las seis perillas apagadas. O sea que
# **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` contesta, y el binario es x86-64
# pelado: **cero `%ymm`, cero `vfmadd`, cero `roundps`** en 1.634.770 líneas de `objdump -d`,
# contadas sobre el artefacto sellado. 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 y NO se confía en el default de nadie. Y por eso 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, no deducido: `momento`/gioser y el worker `dev.gioser.net`
# traen los seis flags, y el objetivo de metal (TigerLake, ver [[etapa-metal-usb]]) también.
# Bajar el piso o publicar variantes es `docs/plan-variantes-cpu.md`, no esta receta.
# `-DGGML_NATIVE=OFF` se queda igual, pero por lo que de verdad hace acá: prohibir `-march=native`,
# que sería un artefacto distinto por máquina — la familia del `MAKE_STAMP` de nftables.
#
# 3. **`-DGGML_CCACHE=OFF` — viene ON de fábrica y se autodetecta.** Si algún día el lab trae
# `ccache`, el build cambia de forma sin que el corpus haya cambiado en nada, y el lab NO entra
# en `hash_inputs` (ver [[lab-fuera-de-hash-inputs]]): dos labs sellarían bytes distintos en la
# misma dirección. Misma familia que el `CONFIG_SHELL=bash` que se rechazó en nftables.
#
# 4. **`-DGGML_OPENMP=OFF` — no es recorte, es evitar una dep invisible.** Con OpenMP encendido el
# binario arrastra la runtime de OpenMP del compilador. ggml trae su PROPIO pool de hilos y lo usa
# cuando OpenMP no está; el corpus prefiere no meterse la runtime de otro toolchain adentro
# (ver el `-static-libstdc++` de cmake.toml, misma lección).
#
# `-DLLAMA_OPENSSL=OFF`: su único uso es HTTPS para BAJAR modelos de HuggingFace desde el propio
# binario. Acá un modelo entra como entra cualquier fuente —pineado por sha256— así que esa función
# no sólo no hace falta: no queremos que exista. `-DLLAMA_BUILD_APP=OFF` por lo mismo (`app/` es el
# binario unificado con `download.cpp` adentro).
#
# ⚠ `LLAMA_BUILD_TOOLS=ON` NO es un extra: `llama-server` vive en `tools/server`, y `tools/CMakeLists.txt`
# sólo entra a `server/` (y a `cli/`) si `LLAMA_BUILD_SERVER` está encendido DENTRO de TOOLS. Con
# TOOLS=OFF no hay servidor. De yapa vienen `llama-quantize`, `llama-bench` y `llama-perplexity`,
# que son las tres herramientas con las que se mide un modelo antes de pinearlo.
#
# El wrapper `.zwrap` es el mismo de `poppler-glib` y por lo mismo: cmake le pasa
# `-Wl,--fatal-warnings` al linker y zig cc convierte avisos benignos en errores.
#
# El `install` comprueba que `llama-server` EXISTA. No es paranoia: si `LLAMA_BUILD_TOOLS` o
# `LLAMA_BUILD_SERVER` se apagan solos —que es lo que hizo `ENABLE_GLIB` en poppler—, el build sale
# 0 y sella un artefacto sin la única pieza por la que esta receta existe.
name = "llama-cpp"
version = "b10901"
license = "MIT"
[source]
tarball = "https://github.com/ggml-org/llama.cpp/archive/refs/tags/b10901.tar.gz"
sha256 = "2cf2d648c6a8ee94f67a94d8c70479620ba481e01a1d930a9075bc7780ff4f9f"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
flags = []
# Sin esto el artefacto pesa 1,8 G: 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++. Medido: `llama-server`
# pasa de 133 M a una fracción. Entra en el hash (`recipe.rs:574`), como tiene que ser.
strip_debug = true
[build.phases]
configure = '''
ZW="$PWD/.zwrap"; mkdir -p "$ZW"
cat > "$ZW/cc" <<'W'
#!/bin/sh
a=""; for x in "$@"; do [ "$x" = "-Wl,--fatal-warnings" ] && continue; a="$a $x"; done
exec /opt/zig/zig cc -mcpu=baseline $a
W
cat > "$ZW/cxx" <<'W'
#!/bin/sh
a=""; for x in "$@"; do [ "$x" = "-Wl,--fatal-warnings" ] && continue; a="$a $x"; done
exec /opt/zig/zig c++ -mcpu=baseline $a
W
chmod +x "$ZW/cc" "$ZW/cxx"
cmake -B build -G Ninja -Wno-dev \
-DCMAKE_C_COMPILER="$ZW/cc" -DCMAKE_CXX_COMPILER="$ZW/cxx" \
-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF -DBUILD_SHARED_LIBS=OFF \
-DGGML_NATIVE=OFF -DGGML_CCACHE=OFF -DGGML_OPENMP=OFF -DGGML_BLAS=OFF \
-DGGML_SSE42=ON -DGGML_AVX=ON -DGGML_AVX2=ON -DGGML_BMI2=ON -DGGML_FMA=ON -DGGML_F16C=ON \
-DGGML_RPC=OFF -DGGML_BACKEND_DL=OFF \
-DLLAMA_OPENSSL=OFF -DLLAMA_BUILD_UI=OFF -DLLAMA_USE_PREBUILT_UI=OFF \
-DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_APP=OFF \
-DLLAMA_BUILD_TOOLS=ON -DLLAMA_BUILD_SERVER=ON
'''
compile = 'cmake --build build -j"$(nproc)"'
install = '''
set -e
DESTDIR=/out cmake --install build
# Ver la cabecera: si TOOLS o SERVER se apagan solos, esto sella sin la pieza que justifica la receta.
test -x /out/usr/bin/llama-server || {
echo "ERROR: no se instaló llama-server — ¿LLAMA_BUILD_TOOLS/LLAMA_BUILD_SERVER apagados?" >&2
exit 1
}
test -x /out/usr/bin/llama-cli || {
echo "ERROR: no se instaló llama-cli" >&2
exit 1
}
# Ver la perilla 2 de la cabecera: SOURCE_DATE_EPOCH apaga INS_ENB y el binario sale sin una sola
# instrucción vectorial, sellando igual y corriendo igual. Esto es lo único que lo ve.
objdump -d build/ggml/src/libggml-cpu.a 2>/dev/null | grep -q '%ymm' || {
echo "ERROR: libggml-cpu.a NO tiene registros %ymm — AVX2 quedó apagado (¿INS_ENB=OFF?)" >&2
exit 1
}
objdump -d build/ggml/src/libggml-cpu.a 2>/dev/null | grep -q 'vfmadd' || {
echo "ERROR: libggml-cpu.a NO tiene vfmadd — FMA quedó apagado" >&2
exit 1
}
'''
[deps]
build = ["cmake", "samurai", "busybox", "binutils", "pkgconf"]