Saltar al contenido

Lección 20 · 24 en la ruta · 16 min de lectura · Nivel 2

Modelos locales, ronda 2

Catálogo, banco de pruebas y elección automática de modelo por tarea, medidos en la Mac

La historia detrás de esta lección El vigilante que mataba al proceso equivocado

Dejé una Mac mini de 16 GB midiendo modelos locales toda la noche: quince modelos, uno a la vez, con un vigilante que mata el proceso si el swap crece más de 3 GB. Dos días después, el monitor de Windows llevaba horas avisando “mac: caída”. Los pasos con los comandos exactos están en el tutorial 21, pasos 8 a 12.

Primera pista falsa: la Mac no estaba caída

ssh mac no respondía y el ping por IPv4 tampoco. Por IPv6 link-local, en cambio, la Mac contestaba, y llevaba un día y 21 horas encendida. El problema era la VPN del trabajo: anunciaba la misma red de mi casa (/24) con métrica 1, así que todo el tráfico hacia la Mac entraba al túnel y moría ahí. La solución temporal fue un alias de SSH a la dirección IPv6, con HostKeyAlias para que siguiera verificando la misma llave de siempre.

La lección es barata, pero la olvido seguido: antes de culpar a una máquina, hay que mirar por dónde viajan los paquetes (route print).

Segunda pista falsa: “se apagó”

La corrida no murió por un apagado. Murió por un kernel panic a las 03:23 de la primera noche:

panic(...): watchdog timeout: no checkins from watchdogd in 92 seconds

El sistema estuvo tan ahogado que el proceso que avisa “sigo vivo” no pudo correr en minuto y medio, y el kernel reinició la Mac. El log del banco tenía un culpable a mano: el modelo de 14 B que estaba cargando en ese momento. Era la explicación cómoda, y no era la correcta.

Lo que contaban los archivos

El log decía que casi todos los modelos habían muerto por “swap +3 GB” a los 10–40 segundos. Sin embargo, los archivos de resultados tenían otra hora:

llm_alt:qa   killed swap +3792 MB …  wall=10.1 s     # muerto hacia las 02:56
03:08:54 llm_alt-qa.json                              # …pero escribió su resultado a las 03:08

Un proceso muerto no escribe doce minutos después.

El banco lanza cada modelo como /usr/bin/time -l python …, porque time es lo que mide la memoria pico contando la GPU. El vigilante hacía kill sobre time. time moría, pero no le pasaba la señal a su hijo, así que el modelo quedaba huérfano, seguía trabajando, y el banco cargaba el siguiente encima. A las tres de la mañana había varios modelos de 9 a 14 B en 16 GB. El pánico no lo causó un modelo: lo causó el vigilante.

El arreglo son dos líneas: el hijo nace en su propio grupo de procesos y se mata el grupo entero.

p = subprocess.Popen(cmd, ..., start_new_session=True)
os.killpg(p.pid, signal.SIGKILL)       # antes: p.kill(), que solo mataba a `time`

Antes de volver a gastar horas de banco, lo probé con algo que no fuera un modelo: --timeout 3 -- sleep 97 terminó a los 4,1 s, y pgrep no encontró ningún sleep. El gestor de modelos tenía el mismo patrón y el mismo bug, así que también se arregló.

También desconfiar de los “ok”

Los modelos que habían “pasado” esa noche corrieron con huérfanos al lado. Los volví a medir con la Mac limpia:

primera nocheMac limpia
texto llm, velocidad20,3 tok/s37,2 tok/s
visión vlm, carga473 s0,8 s
visión vlm, por imagen3,9 s1,9 s

La calidad era la misma, pero la velocidad estaba a la mitad. Esos datos parecían buenos, y el catálogo los habría usado para elegir.

Qué rinde, medido bien

Con el bug arreglado, cada modelo corrió solo. El catálogo del gestor ahora elige por medición en cuatro tareas:

  • Texto y código: llm_alt (Gemma de 12 B). Acierta el 97 % en preguntas y el 75 % en código. El modelo “especializado en código” sacó 33 %, y el pequeño de 4 B, 8 %.
  • Visión: vlm, 100 % de aciertos a 1,9 s por imagen. Para OCR masivo, uno de 3 GB es cuatro veces más rápido con 62 % de aciertos.
  • Transcripción: stt, WER 3 %, 17 segundos de audio por segundo.
  • Imagen: ninguno cabe. Generan bien (todas pasaron el chequeo), pero ocupan 12–13 GB y el presupuesto es 12. Funcionan solo con la Mac vacía.

Dos hallazgos que valen más que la tabla:

  1. Reutilizar el prefijo. Con un documento de 32 000 tokens, la primera pregunta tarda 162 s porque el modelo tiene que leerlo entero. Si esa lectura se guarda en caché y se reutiliza, las preguntas siguientes tardan 0,34 s. Para hacerle varias preguntas a un mismo texto largo, no hay palanca más grande.
  2. La GPU que ya tienes. El mismo efecto cómic sobre 10 s de video tarda 13,4 s con un filtro de CPU y 1,07 s con Core Image. Es 12 veces más rápido sin instalar nada.

Lo que no salió: el texto a video con LTX no cabe en 16 GB con apps abiertas, porque el vigilante mató el paso más pequeño a los 20 s. Y el modelo de 14 B con contexto largo no lo volví a correr solo. La causa del pánico ya está arreglada, pero no voy a probarlo en una Mac compartida sin nadie mirando.

¿Y si el local se rinde y le pasa la tarea a Claude?

Probé una cascada: el modelo local responde primero y, si no está seguro, dice NO_PUEDO y la tarea pasa a Claude. Con 42 tareas, el local acertó 38 y no se rindió ni una vez: aceptó sus 4 errores con la misma seguridad que sus aciertos. Claude, con todas, acertó 42 en 92 segundos, por 27 centavos de dólar. Escalar según la confianza que el modelo dice tener no sirve. Para el código hay una señal mejor: correr los tests y escalar solo lo que falla.

Lo que me llevo

Un vigilante que no verifica que su víctima murió es peor que no tener vigilante. Con él, el número dice “controlado” mientras la máquina se llena. Y la evidencia útil no estaba en el log del programa, que mentía con toda la seguridad, sino en algo que el programa no controlaba: la hora en que se escribió cada archivo.

Fotogramas de la portada: Big Buck Bunny, © Blender Foundation, CC-BY 3.0.

Objetivo

Pasar de “cinco modelos elegidos a mano” a un catálogo que se pueda consultar, un banco de pruebas que mida a todos con las mismas entradas y un gestor que elija solo el modelo de cada tarea, explicando por qué.

Por qué

Con 16 GB de memoria unificada, el límite no es el disco (la Mac tiene ~240 GB libres) sino la memoria: Metal deja usar unos 12,7 GB. Un modelo puede brillar en su ficha y no caber, o caber y equivocarse en español. La única forma honesta de elegir es medir: calidad con respuesta conocida, velocidad, memoria pico y tiempo de carga. Y fijar antes el umbral que separa “sirve” de “ninguno sirve”.

Repo: IchiSieben/modelos-mac. Viene del tutorial 16 (gestor, cola, un modelo a la vez).

Pasos

1. Buscar candidatos sin elegir de memoria

Dos agentes investigaron en paralelo (web, código de los runtimes, API de Hugging Face). Uno cubrió texto, código, visión, imagen y edición. El otro cubrió transcripción, hablantes y video. Después un script volvió a verificar todo contra la API, porque la prosa de un agente no cuenta como fuente:

win$ python scripts/catalog_check.py
llm_alt           9.04 GB  apache-2.0             gated=False 2026-09-15  mlx-community/gemma-4-12B-it-qat-OptiQ-4bit
ministral14       8.46 GB  apache-2.0             gated=False 2025-12-03  mlx-community/Ministral-3-14B-Instruct-2512-4bit
gemma_coder       6.73 GB  apache-2.0             gated=False 2026-06-21  mlx-community/gemma-4-12b-coder-fable5-composer2.5-4bit
minicoder         3.00 GB  mit                    gated=False 2026-07-01  mlx-community/mini-coder-4b-OptiQ-4bit
paddleocr         0.72 GB  apache-2.0             gated=False 2026-08-12  mlx-community/PaddleOCR-VL-1.6-4bit
glmocr            1.25 GB  mit                    gated=False 2026-02-03  mlx-community/GLM-OCR-4bit
qwen_asr          2.47 GB  apache-2.0             gated=False 2026-01-29  mlx-community/Qwen3-ASR-1.7B-8bit
granite_asr       4.52 GB  apache-2.0             gated=False 2026-05-28  mlx-community/granite-speech-4.1-2b-nar-mlx
zimage            5.90 GB  apache-2.0             gated=False 2026-09-26  mflux-community/z-image-turbo-mflux-q4
diar_alt          0.03 GB  cc-by-4.0              gated=auto  2025-09-29  pyannote/speaker-diarization-community-1  <-- CHECK
ltx              59.69 GB  other                  gated=False 2026-08-15  dgrauet/ltx-2.3-mlx-q4  <-- CHECK

El script marca dos casos: pyannote, porque es gated y exige un token del dueño (por eso no se baja), y LTX, por su licencia propia y porque el repo trae varias variantes que suman 59,7 GB. La tabla completa, con fuentes, está en modelos-mac/docs/CATALOGO.md.

Hallazgo que cambia el punto 5 del encargo: Nemotron no acepta número de hablantes. La firma real en mlx-audio 0.5.7 es generate(audio, sample_rate=16000, threshold=0.5, min_duration=0.0, merge_gap=0.0, verbose=False). El modelo tiene 8 salidas fijas y decide solo cuántas usa.

2. Bajar con piso de 100 GB y sha256

El piso subió de 60 a 100 GB en server/models.toml ([budget] min_free_gb). Simulacro primero:

mac$ python scripts/download.py --dry-run --only llm_alt ministral14 gemma_coder minicoder paddleocr glmocr qwen_asr granite_asr zimage diar_wespeaker
...
total 42.08 GB; free now 256.8 GB; after 214.7 GB

El primer intento falló después de bajar 9 GB:

KeyError: 'task'
EXIT=1

Las entradas nuevas usan tasks = [...] (un modelo puede servir a varias tareas: Gemma-4-12B sirve para texto, código y visión), y download.py seguía leyendo m["task"]. Se arregló en una línea (m.get("task") or m.get("tasks")) y se relanzó. Lo ya bajado no se volvió a descargar: se verificó contra el sha256 de LFS en segundos (ok llm_alt 9.04 GB in 4.3 s).

ok ministral14 8.46 GB in 243.5 s
ok gemma_coder 6.73 GB in 218.8 s
ok minicoder 3.00 GB in 187.6 s
ok paddleocr 0.72 GB in 33.2 s
ok glmocr 1.25 GB in 36.8 s
ok qwen_asr 2.47 GB in 55.7 s
ok granite_asr 4.52 GB in 208.6 s

3. Una descarga en paralelo frena el respaldo (y se pausa)

Mientras bajaban los modelos, otro proceso copiaba el respaldo de fotos de la Mac a F:\ de Windows por SSH. En ese momento era la única copia de 222 GB. La velocidad del espejo cayó de 48 a 2 MB/s. El disco externo seguía leyendo a ~30 MB/s, así que el cuello de botella era la red. Se pausó la descarga de video (kill -STOP <pid>, se reanuda con kill -CONT):

win$ # 60 s con la descarga de LTX pausada
mirror 37 MB/s

Lección: en una red doméstica, un snapshot_download y un ssh | tar se reparten el mismo enlace. Lo irremplazable va primero.

4. LTX: bajar solo lo que se usa

ltx-2-mlx llama a snapshot_download(repo) sin filtro. Para la ruta --distilled eso son 59,7 GB, cuando hacen falta 20,5. Se añadió allow (patrones de archivos) a download.py:

mac$ python scripts/download.py --dry-run --only ltx ltx_gemma
ltx          dgrauet/ltx-2.3-mlx-q4                            20.48 GB  commit 56a5866d63
ltx_gemma    mlx-community/gemma-3-12b-it-4bit                  8.07 GB  commit 86cc6a8ded
total 28.55 GB; free now 205.8 GB; after 177.2 GB

El primer ltx-2-mlx info alcanzó a dejar 3,5 GB en la caché de Hugging Face antes de cortarse. Se anotó en NEEDS-OWNER; no se borró. Desde ahí la prueba corre con HF_HUB_OFFLINE=1 y rutas locales.

5. Referencia de transcripción: audio real, transcripción conocida

Kokoro (voz sintética) favorece el WER. Por eso se usó FLEURS es_419 (Google, CC-BY-4.0): personas reales leyendo frases con transcripción publicada. scripts/prep_fleurs.py toma las primeras 40 frases distintas del split dev (orden por nombre de archivo, determinista).

Falló la primera vez:

urllib.error.ContentTooShortError: <urlopen error retrieval incomplete: got only 234975905 out of 255285841 bytes>

urlretrieve no reanuda. Se cambió por curl --retry 5 -C -:

40 reference clips, 473 s; long 1426.260000 s

6. Pruebas de humo (sin tiempo oficial: el espejo seguía cargando la Mac)

qa   ok  aciertos 2/2   36.5 tok/s
ctx  @2k: lectura 217 tok/s, generación 38.5 tok/s, primer token 16.2 s
     mismo prefijo reutilizado: primer token 0.24 s (tras un primer uso de 10.8 s)
     @8k: primer token 39.5 s → 0.25 s reutilizando el prefijo

El registro completo de 5000 líneas no cabe. Son unos 125 000 tokens. Con Qwen3.5-9B:

         12803904480  peak memory footprint
vm.swapusage: total = 10240.00M  used = 9707.25M

12,2 GB de footprint y 9,7 GB de swap en 5 minutos, sin terminar de leer el prompt. Se mató por PID. La prueba oficial se reordenó: primero “recortado” y “trozos”, con resultado guardado a medias, y al final “completo”, bajo un vigilante que mata el proceso si el swap crece más de 3 GB.

7. Arranque desatendido: la Mac espera sola

La corrida (unas 6 h) no puede depender de que esta ventana de Windows siga abierta. Por eso scripts/sesion_g_start.sh corre dentro de un tmux de la Mac y hace tres cosas: espera a que desaparezca el tar que alimenta el espejo, reanuda la descarga pausada (kill -CONT) y lanza sesion_g_run.sh. El fin del espejo se detecta desde la Mac: el emisor es el tar local.

Dos tropiezos reales al lanzarlo:

mac$ tmux has-session -t sesion-g && echo EXISTS
EXISTS                      # ¡falso! existía "sesion-g-dl2"

-t acepta un prefijo. Para pedir el nombre exacto se usa -t =sesion-g, pero en zsh:

zsh:1: sesion-g not found

En zsh, =palabra se expande a la ruta del comando palabra. Hay que ponerlo entre comillas:

mac$ tmux has-session -t '=sesion-g' || tmux new -d -s sesion-g 'bash scripts/sesion_g_start.sh'
mac$ tmux ls
sesion-g: 1 windows (created Mon Oct  5 02:28:45 2026)
mac$ cat ~/modelos/logs/sesion-g-start.log
2026-10-05 02:28:45 start; waiting for mirror sender: 35851

Ver el avance desde Windows, con un solo comando:

win$ ssh mac 'tail -n 25 ~/modelos/logs/sesion-g-start.log ~/modelos/logs/sesion-g-run.log'

8. “La Mac está caída” (no lo estaba) y el pánico que sí ocurrió

Dos días después, el vigilante de Windows llevaba horas avisando mac: down y ssh mac daba timeout. Antes de culpar a la Mac:

win$ ping -n 1 <mac>.local
Reply from fe80::…%4: time=129ms          # por IPv6 responde
win$ ping -4 -n 1 <mac>.local
Request timed out.                          # por IPv4, no
win$ route print -4 | grep "<red-casa>"
      <red-casa>    <máscara /24>         On-link     <ip-windows>    281
      <red-casa>    <máscara /24>         On-link     <ip-vpn>      1

La segunda ruta es de la VPN corporativa (adaptador “PANGP”): reclama la red de casa con métrica 1, así que todo el IPv4 hacia la Mac se mete al túnel y muere ahí. La dirección IPv6 link-local (fe80::) no pasa por la VPN. Sin tocar la VPN, un alias de SSH para esos días:

Host mac6
  HostName fe80::<id>%%4          # %% porque ssh_config usa % para sus variables
  HostKeyAlias <ip-de-la-mac>     # verifica la MISMA llave de host que ya conocíamos
  User <usuario>
  IdentityFile ~/.ssh/id_mac

HostKeyAlias importa: sin él, ssh no reconoce la dirección nueva y se niega a entrar (Host key verification failed). Lo tentador sería desactivar la verificación; lo correcto es decirle qué llave conocida debe esperar.

Ya dentro, la Mac llevaba 1 día y 21 horas encendida. La corrida no murió por un apagado:

mac$ last reboot | head -1
reboot time                                Mon Oct  5 03:22
mac$ ls /Library/Logs/DiagnosticReports/ | grep panic
panic-full-2026-10-05-032324.0002.panic
  "panicString" : "panic(…): watchdog timeout: no checkins from watchdogd in 92 seconds"

Un watchdog panic: el sistema estuvo tan ahogado que el proceso que avisa “sigo vivo” no pudo correr en 92 s, y el kernel reinició la máquina.

9. El vigilante que mataba al proceso equivocado

El log decía que casi todos los modelos habían muerto por “swap +3 GB” a los 10–40 s. Pero el disco contaba otra historia:

mac$ grep llm_alt:qa ~/modelos/logs/sesion-g-run.log
llm_alt:qa   killed swap +3792 MB …  wall=10.1 s           # muerto hacia las 02:56
mac$ stat -f "%Sm %N" -t "%H:%M:%S" bench-runs/2026-10-05/llm_alt-qa.json
03:08:54 llm_alt-qa.json                                   # …pero escribió 12 min después

Un proceso “matado” no escribe su resultado doce minutos más tarde. La causa: el banco lanza cada modelo como /usr/bin/time -l python bench_child.py … para medir la memoria pico, y el vigilante hacía p.kill() sobre time. time muere, pero no reenvía la señal: el hijo con el modelo queda huérfano (lo adopta launchd) y sigue trabajando. El banco esperaba 10 s y cargaba el siguiente modelo encima. A las 03:19 había varios modelos de 9–14 B en 16 GB → pánico.

Al relanzar con el swap en cero, el patrón se repitió en minutos (22,8 GB de swap, 4 % de memoria libre) y paró en cuanto se mataron los huérfanos. El arreglo es de dos líneas: poner al hijo en su propio grupo de procesos y matar el grupo entero.

p = subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT, start_new_session=True)
...
os.killpg(p.pid, signal.SIGKILL)      # antes: p.kill()  → solo moría `time`

Prueba de que funciona, antes de volver a gastar horas de banco:

mac$ python scripts/run_watched.py --label t2 --out /tmp/t2.jsonl --timeout 3 -- sleep 97
{"label": "t2", "rc": -9, "wall_s": 4.1, "killed": "timeout", …}
mac$ pgrep -fl "sleep 97"
                                      # vacío: no queda nadie

Lección: un vigilante que no verifica que su víctima murió es peor que no tener vigilante, porque los números dicen “controlado” mientras la máquina se llena. Y los resultados de una corrida así no valen: todos los “killed” de esa noche se volvieron a medir.

10. Reanudar solo lo que falta (y desconfiar también de los “ok”)

sesion_g_run.sh no se puede reanudar: relanzarlo repite lo hecho y vuelve a entrar al paso del pánico. Por eso se escribieron dos scripts nuevos que usan el mismo banco, el mismo vigilante y el mismo log, y solo corren los pares sin resultado válido:

mac$ tmux new -d -s sesion-g "bash scripts/sesion_g2_run.sh"
mac$ tmux new -d -s sesion-g "bash ~/.enjambre/busy.sh run bench sesion-g2b -- bash scripts/sesion_g2b_run.sh"

El segundo va envuelto en busy.sh: mientras corre, la Mac publica ~/.enjambre/busy.json y el enrutador de enjambre no le manda renders con GPU ni trabajos de modelos.

Un “ok” también puede estar contaminado. Los pares que pasaron el 05-10 corrieron mientras los huérfanos de los anteriores seguían vivos. Medidos otra vez con la Mac limpia:

05-10  llm:qa      ok  speed=20.3 tok/s  …
07-10  llm:qa      ok  speed=37.2 tok/s  …                    # casi el doble
05-10  vlm:vision  ok  speed=3.87 s/imagen  load=473.27 s
07-10  vlm:vision  ok  speed=1.86 s/imagen  load=0.81 s

La calidad era la misma, pero la velocidad estaba a la mitad. Si se hubieran publicado los números del 05-10, el catálogo habría elegido con datos falsos.

Dos tropiezos más, de los baratos:

  • La URL del clip de prueba (Big Buck Bunny) ahora da 404. El archivo sigue en el sitio de Blender, pero solo como .zip:
    curl: (56) The requested URL returned error: 404
    
    El script ahora baja el .zip y extrae el mp4.
  • El filtro coreimage de ffmpeg no acepta el nombre a secas:
    [coreimage @ …] Parsing of filters failed.
    
    Necesita filter=CIComicEffect@default.

Y uno que salvó datos: la prueba de contexto largo solo guardaba al final. Cuando Metal se quedó sin memoria en 32k, perdía también los resultados de 2k y 8k. Ahora guarda después de cada tamaño, y en la segunda pasada llm_alt dejó sus filas de 2k y 8k antes del error:

RuntimeError: [METAL] Command buffer execution failed: Insufficient Memory

El banco escribe una fila por modelo y tarea en measurements.jsonl, y scripts/banco_table.py la convierte en las tablas de modelos-mac/docs/BANCO.md. Resumen:

tareaeligecalidadpor qué
textollm_alt0,967el que más acierta (llm acierta 0,867, pero va al doble de velocidad)
códigollm_alt0,75el único sobre el umbral de 0,50; el “especializado en código” sacó 0,33
visiónvlm1,0glmocr acierta 0,625, pero es 4× más rápido para OCR
transcripciónstt0,969qwen_asr empata, pero ocupa 13,7 GB
imagen—1,0”ninguno sirve”: las tres ocupan más de 12 GB (funcionan, pero no caben)

El gestor lee esas filas en vivo. Sin reiniciarlo:

mac$ curl -s 127.0.0.1:8765/catalog
"text": {"model": "llm_alt", "reason": "medido el 2026-10-07: aciertos QA 0.967, 18.8 tok/s gen,
         9820.7 MB; mejor de 3 que caben en 12000 MB"}

Antes decía predeterminado de models.toml en todas las tareas.

Cascada local → Claude (scripts/bench_cascade.py, desde Windows por túnel):

local    ok=38/42  1102.2 s  US$ 0       declined=0
claude   ok=42/42    92.2 s  US$ 0.2692
cascade  ok=38/42  1102.2 s  US$ 0       sent_to_claude=0
calibration: accepted_and_wrong=4

El modelo local nunca dijo NO_PUEDO, así que la cascada no escaló nada. La autoconfianza no sirve como señal. Para el código, la señal útil es que fallen los tests.

Contexto largo: con 32 000 tokens, llm tarda 162 s en dar la primera respuesta porque tiene que leer todo el documento. Si el documento se procesa una vez y su caché se reutiliza, las preguntas siguientes tardan 0,34 s. Para preguntar varias cosas sobre un mismo texto largo, esa es la palanca.

12. Hablantes, video y cuánta memoria queda

Hablantes (56 min de audio real, cuatro trabajos del gestor, uno tras otro):

trabajotiempohuella del proceso
transcripción138 s5,3 GB
Nemotron13 s2,2 GB
Sherpa496 s0,9 GB
WeSpeaker415 s0,7 GB

Sherpa y WeSpeaker discrepan solo el 7 % del tiempo, y Nemotron discrepa de ambos un 35 %. Cuál acierta no se decide con números sino escuchando: la hoja de revisión marca los 5 minutos que hay que oír, y esa decisión es del owner.

Video: el mismo efecto cómic tarda 13,4 s en CPU (edgedetect) y 1,07 s en GPU (Core Image). Es 12 veces más rápido sin cambiar de máquina. En cambio, el texto a video con LTX no cabe: el vigilante mató el paso más pequeño a los 20 s.

Memoria para Blender, medida con memory_pressure mientras corría un trabajo de texto:

03:05:04 idle     free_pct=87%
03:05:13 running  free_pct=26%      # llm_alt cargado: huella 9,8 GB
03:06:02 after    free_pct=86%

El gestor no deja ningún modelo residente: cada trabajo es un proceso que termina. Mientras corre un trabajo de texto quedan unos 4 GB, así que Blender con Metal solo entra si la escena es pequeña. Si no, --device CPU o esperar a que busy.json desaparezca.

Cierre. Lo que no se pudo medir desatendido queda anotado con su comando en NEEDS-OWNER §13: ministral14 con contexto largo, LTX con la Mac vacía y la elección de diarizador.

Qué salió mal de verdad

Todo esto ocurrió y está descrito en los pasos de arriba:

  1. download.py falló con KeyError: 'task' tras bajar 9 GB: las entradas nuevas usan tasks = [...] (paso 2).
  2. Una descarga en paralelo bajó el respaldo de 48 a 2 MB/s; se pausó con kill -STOP (paso 3).
  3. urlretrieve truncó FLEURS (ContentTooShortError); se cambió por curl --retry 5 -C - (paso 5).
  4. El registro de 5000 líneas llevó a Qwen3.5-9B a 12,2 GB de huella y 9,7 GB de swap (paso 6).
  5. tmux has-session -t sesion-g acertó por prefijo, y -t =sesion-g falló en zsh sin comillas (paso 7).
  6. La VPN corporativa hizo que la Mac pareciera caída; el alias mac6 por IPv6 la alcanzó (paso 8).
  7. El vigilante mataba a time y dejaba huérfano al hijo con el modelo: pánico del kernel por watchdog. Se arregló con start_new_session=True y os.killpg (pasos 8 y 9).
  8. Los “ok” del 05-10 estaban contaminados por huérfanos (velocidad a la mitad) y se re-midieron el 07-10 (paso 10).
  9. La URL de Big Buck Bunny dio 404 y el filtro coreimage necesita filter=CIComicEffect@default (paso 10).
  10. Metal se quedó sin memoria a 32k y se perdían los resultados de 2k y 8k; ahora se guarda tras cada tamaño (paso 10).

Cómo verificar

Tres comandos y su salida esperada, copiados de los pasos:

win$ python scripts/catalog_check.py
llm_alt           9.04 GB  apache-2.0             gated=False 2026-09-15  mlx-community/gemma-4-12B-it-qat-OptiQ-4bit
...
diar_alt          0.03 GB  cc-by-4.0              gated=auto  2025-09-29  pyannote/speaker-diarization-community-1  <-- CHECK
mac$ python scripts/run_watched.py --label t2 --out /tmp/t2.jsonl --timeout 3 -- sleep 97
{"label": "t2", "rc": -9, "wall_s": 4.1, "killed": "timeout", …}
mac$ pgrep -fl "sleep 97"
                                      # vacío: no queda nadie
mac$ curl -s 127.0.0.1:8765/catalog
"text": {"model": "llm_alt", "reason": "medido el 2026-10-07: aciertos QA 0.967, ..."}

Antes de la medición, la razón decía predeterminado de models.toml en todas las tareas.

Autoevaluación

  1. ¿Por qué hay que fijar el umbral de “sirve” antes de medir? Respuesta: para poder declarar “ninguno sirve” sin ajustar el criterio a los resultados; en imagen las tres opciones ocupan más de 12 GB y por eso no se elige ninguna.
  2. ¿Por qué el vigilante anterior dejaba procesos huérfanos? Respuesta: mataba a /usr/bin/time, que no reenvía la señal al hijo con el modelo; el arreglo es poner al hijo en su propio grupo de procesos y matar el grupo entero.
  3. ¿Por qué se descartaron los “ok” del 05-10? Respuesta: corrieron mientras los huérfanos seguían vivos; la calidad era igual pero la velocidad estaba a la mitad (20,3 contra 37,2 tok/s en llm:qa).

Pendiente (NEEDS-OWNER §13): la elección del diarizador, ministral14 con contexto largo y LTX con la Mac vacía.

Se guarda solo en este navegador.