Dos Mac mini en casa: uno sirve, el otro trabaja y piensa

Dos Mac mini en casa: uno sirve, el otro trabaja y piensa
ContenidoContents

Estos días he movido cosas entre dos Mac mini.

Hasta ahora el Mac mini M4 lo hacía todo: servía la web y el blog, el fan site de Apple, los scripts programados y los avisos por Telegram. También todo lo de LiftCoach, mi app de gimnasio: la API con la que se sincroniza (entrenos, rutinas, nutrición y salud, en SQLite) y su servidor MCP con OAuth, los dos con launchd detrás de Apache. Y además era donde programaba. Con el M6 en casa, toca repartir.

El reparto

El M4 pasa a servidor sin pantalla. Web, blog, onemore, los scripts con launchd y Telegram. Lo de siempre, pero sin que yo esté encima con el editor abierto. Cómo se sostiene eso desde casa lo conté en Self-hosting sin miedo.

El M6 es la máquina de trabajo. Ahí programo, y ahí vive Ollama (0.40.13), escuchando en http://Mac-mini-M6.local:11434.

Los scripts del M4 no llevan modelo propio. Llaman al Ollama del M6 por la red de casa. El servidor sirve; el otro piensa.

Mac mini M4 Mac mini M6
Papel servidor sin pantalla trabajo y Ollama
Chip Apple M4 Apple M6
CPU 10 núcleos: 4 de rendimiento y 6 de eficiencia 12 núcleos: 2 Super, 4 de rendimiento y 6 de eficiencia
GPU 10 núcleos 12 núcleos
Memoria unificada 16 GB 32 GB
Disco 256 GB 1 TB
Sistema macOS 27.2 macOS 27.2

Los 32 GB del M6 son los que permiten tener cargado gemma4, que ocupa 7,7 GB, sin quitarle memoria a lo que estoy haciendo.

Cambio de modelos

El 30 de octubre cambié los modelos de los scripts. Ya había probado IA local en el Mac mini. Antes de escribir esto lo medí con los datos reales del resumen de hoy, tres veces con cada modelo, y el pequeño no se quedaba tan corto como pensaba: ninguno inventó cifras y los dos tardaron parecido, entre 7 y 15 segundos.

La diferencia está en la constancia. gemma4 dio las tres veces el mismo formato y casi la misma longitud, de 191 a 193 tokens. mistral osciló entre 180 y 358, y en una pasada se inventó un apartado «Disco:» que no estaba en el formato y se olvidó de los ataques críticos. gemma4 tampoco es perfecto: en una pasada mezcló las 6 amenazas de Cloudflare con las 15 IPs de los ataques. Para un parte que leo cada mañana, prefiero el que sale igual todos los días.

Tres scripts generan texto: el resumen diario, el contexto que acompaña a las alertas y el detector de fallos. Pasan de mistral:7b a gemma4:12b, que ocupa 7,7 GB.

mistral:7b gemma4:12b
Parámetros 7.200 millones 12.400 millones
Tamaño en disco 4,4 GB 7,7 GB
Contexto 32.768 tokens 262.144 tokens
Cuantización Q4_K_M NVFP4
Capacidades texto y herramientas texto, imagen, audio, herramientas y razonamiento
Licencia Apache 2.0 Apache 2.0

Con gemma4 hay un detalle: hay que mandarle "think": false. Si no, piensa en voz alta y el razonamiento acaba en el mensaje.

1{
2  "model": "gemma4:12b",
3  "prompt": "...",
4  "stream": false,
5  "think": false
6}

Los artículos relacionados del blog y de onemore usan embeddings. Pasan de nomic-embed-text a qwen3-embedding:0.6b, con /api/embed y un prefijo de instrucción delante del texto. Aquí el motivo es más simple: nomic-embed-text tiene 2048 tokens de contexto y, por encima de unos 4000 caracteres, devolvía un error, así que cada post se recortaba a 3500 caracteres o menos antes de compararlo. qwen3-embedding acepta hasta 12000. Los umbrales cambian con el modelo: en el blog, 0,60 para relacionados y 0,68 para el mapa; en onemore, 0,64.

nomic-embed-text qwen3-embedding:0.6b
Parámetros 137 millones 596 millones
Tamaño en disco 274 MB 639 MB
Contexto 2.048 tokens 32.768 tokens
Dimensiones del vector 768 1.024
Cuantización F16 Q8_0

Antes de tocar cada script hice una copia. Es lo mínimo cuando cambias cinco cosas a la vez.

Un token que no debía estar ahí

El token del bot de Telegram estaba escrito dentro del código del script de alertas. Lo saqué a un .env con permisos 600, fuera del código.

Al hacerlo vi otra cosa: el resumen y el detector ni siquiera tenían token. No podían enviar nada. Llevaban un tiempo trabajando para nadie.

El problema de verdad: Red local

El 31 de octubre, a las 8:00, el resumen llegó por Telegram en crudo. Sin redactar. El modelo no había respondido.

En el log, errno 65: No route to host. Pero desde Terminal, en el mismo M4, el M6 respondía sin problema.

La diferencia era quién lanzaba el proceso. macOS tiene un permiso de privacidad, Red local, que decide qué apps pueden hablar con equipos de tu red. Un python lanzado directamente por launchd no podía llegar al M6. Ni el python del sistema ni el python3.14 de Homebrew. Desde Terminal sí, porque ahí manda el permiso de Terminal.

Cambiar al python de Homebrew no lo arregló. Y una curiosidad: ese python ni siquiera aparece en Ajustes → Privacidad y seguridad → Red local. No hay interruptor que activar.

Lo que funcionó: que launchd no lance python, sino /opt/homebrew/bin/bash con un script envoltorio. Ese bash sí tiene el permiso de Red local, y python corre como proceso hijo suyo.

El envoltorio:

1#!/opt/homebrew/bin/bash
2# wrapper.sh: python corre como hijo de este bash, sin exec
3/opt/homebrew/bin/python3 /Users/usuario/bin/mi-script.py "$@"
4exit $?

El detalle importante es no usar exec. Con exec, python sustituye al bash y vuelves a estar como al principio.

El plist apunta al bash, no a python:

 1<key>ProgramArguments</key>
 2<array>
 3  <string>/opt/homebrew/bin/bash</string>
 4  <string>/Users/usuario/bin/wrapper.sh</string>
 5</array>
 6<key>StartCalendarInterval</key>
 7<dict>
 8  <key>Hour</key><integer>8</integer>
 9  <key>Minute</key><integer>0</integer>
10</dict>

Antes de tocar los trabajos de verdad, lo probé con un trabajo temporal de launchd que solo llamaba al modelo y no enviaba nada. Funcionó. Luego pasé los tres scripts al envoltorio.

No es la primera vez que macOS me cambia las reglas del servidor por debajo. La otra fue cuando una actualización tumbó mi servidor.

Cómo queda

Un Mac mini que sirve y no se toca. Otro que trabaja y hace de cerebro para el primero.

Lo que más tiempo me llevó no fue el modelo, fue un permiso de privacidad que no se ve en Ajustes. Si un script de launchd te dice No route to host y desde Terminal todo va, mira quién lanza el proceso.

CompartirShare