Cua en macOS VM: desbloquear Metal y correr LLMs 11x más rápido
Si vienes corriendo modelos en tu Mac, ya te tocó elegir entre dos mundos: o usas el hardware directo (bare-metal, Ollama o llama.cpp nativo) y renuncias al aislamiento, o metes el modelo dentro de una VM para tener snapshots, portabilidad y un límite estricto de recursos, y aceptas que la inferencia va a ir mal. Esa segunda opción era la peor en Apple Silicon: una VM de macOS sobre Apple Silicon típicamente caía al 10-15% del rendimiento bare-metal para inferencia de LLMs. El 11 de agosto el equipo de Cua publicó una capa de compatibilidad que cierra esa brecha: 11.08x más rápido en prompt processing y 16.36x más rápido en token generation con TinyLlama 1.1B, llegando al 98% del bare-metal. Y con Gemma 4 12B cuantizado a 4-bit (6.98 GB), 99.59% en prompts y 94.82% en generación. Es el primer resultado concreto y reproducible de "GPU passthrough usable" en macOS guests, y cambia cómo vamos a pensar los agentes locales.
La pieza técnica es pequeña en líneas de código, pero conceptualmente rompe una pared que el Virtualization.framework de Apple venía sosteniendo en silencio.
Por qué una VM de macOS sobre Apple Silicon era tan lenta
Cuando creas una VM con el Virtualization.framework de Apple en una Mac con chip M-series, el guest ve un dispositivo gráfico paravirtualizado. Apple expone una GPU virtual que el host controla; el guest le envía comandos Metal y el host los ejecuta en la GPU física real. Es el mismo modelo de paravirtualización que usa QEMU con virtio-gpu, no es GPU passthrough al estilo VFIO de Linux.
El detalle que duele es este: cuando el Metal del guest pregunta al dispositivo virtual por sus capacidades, este le responde con un conjunto anclado en una familia antigua de Apple GPU (aproximadamente una Apple 5-era, con 32 KB máximos de threadgroup memory y sin soporte de SIMD-group matrix). El runtime de Metal del guest recibe esa respuesta y elige kernels compatibles con esa familia, no con el hardware real que está del otro lado. Llama.cpp consulta esas capabilities para elegir el path rápido, así que termina ejecutando un kernel antiguo y conservador en hardware perfectamente capaz de correr uno moderno.
En una línea: Apple virtualiza el dispositivo, no el hardware, y la respuesta que recibe el guest le miente sobre qué kernels puede correr.
El resultado en la práctica es que una VM de macOS corriendo llama.cpp con TinyLlama 1.1B procesaba prompts a una fracción mínima del bare-metal. Suficiente para que el proyecto Tart (otro CLI sobre Virtualization.framework) tenga un issue abierto desde hace meses preguntando si el framework va a dar GPU passthrough usable algún día. Spoiler: Apple no va a cambiar el framework. La comunidad va a tener que sortear el límite por arriba, que es exactamente lo que hizo Cua.
Qué construyó Cua: una capa delgada, no un fork del framework
Lo que liberó Cua no es una modificación del Virtualization.framework ni un hypervisor custom. Es una capa de compatibilidad con scope de proceso (process-scoped compatibility layer) que se carga dentro del guest macOS, intercepta las llamadas que hace llama.cpp a Metal y las redirige a fast paths modernos de Metal que el dispositivo virtual dice no soportar, pero que el hardware físico del host ejecuta sin problema.
Tres decisiones de diseño que vale la pena destacar:
- Scope de proceso, no de sistema. No toca el kernel, no requiere kexts, no rompe SIP. Se carga como cualquier librería dinámica y solo afecta al proceso que la linkea. Eso la hace trivial de revertir y de auditar, y permite correr el resto del sistema en el path Metal "stock" sin cambios.
- Release de investigación, no producto. La publican bajo la misma licencia permisiva que Lume y Cua para que la comunidad pueda reproducir los benchmarks y mapear qué chips Apple Silicon, qué versiones de macOS y qué workloads Metal se benefician. Es deliberadamente incompleta: cubre el caso LLM, no promete ser un reemplazo general del stack gráfico.
- Incluye probe de capabilities y logs de benchmark. El repo viene con un binario que reporta exactamente qué familia y qué features anuncia el dispositivo virtual, y con los logs crudos de los benchmarks. Reproducibilidad como valor de diseño, no como promesa de marketing.
Lo que NO es: no es un bypass del sandbox del guest. El host sigue controlando el hardware. La capa expone paths que ya están disponibles en el runtime de Metal; no rompe el modelo de seguridad de Virtualization.framework.
Los benchmarks, con número cerrado
Los dos casos publicados son los que importan para inferencia de LLMs en hardware modesto:
M1 Ultra, TinyLlama 1.1B, cuantización default de llama.cpp:
- Prompt processing: 11.08x más rápido que la VM stock. La VM desbloqueada llega al 98% del bare-metal.
- Token generation: 16.36x más rápido. Es la métrica que más se nota cuando un agente espera el primer token de una respuesta larga.
M1 Ultra, Gemma 4 12B QAT Q4_0 (6.98 GB, modelo de este año):
- Prompt processing: 7.20x más rápido, 99.59% del bare-metal.
- Token generation: 14.54x más rápido, 94.82% del bare-metal.
El segundo caso es el que más me convence: un modelo de 12B cuantizado a 4-bit es justo el tamaño de los agentes locales que mencionamos en Meta Muse Glimmer, y llegar al 95% del bare-metal dentro de una VM significa que puedes correr ese mismo agente aislado, con snapshots, sin pagar el costo de rendimiento que hoy te obliga a elegir entre aislamiento y velocidad.
Para ponerlo en contexto: el post sobre Swiftlet trataba de cómo exprimir hardware limitado (4.3 GB de RAM para un 80B, cuantización agresiva y aprovechar MoE). El ángulo de Cua es el inverso: no comprime el modelo, desbloquea el hardware. Son problemas opuestos y complementarios. Un setup interesante sería Swiftlet + Cua: corres un 80B MoE en una VM de macOS con Metal desbloqueado, aprovechando la cuantización de Swiftlet para que entre en RAM y la capa de Cua para que la VM no sea un cuello de botella de GPU.
Qué cambia para los que construimos agentes locales
Tres cosas concretas que se desbloquean con este enfoque, en orden de impacto práctico:
Sandbox serio sin renunciar al rendimiento. Hoy, si quieres correr un agente LLM que opera sobre tu filesystem, tu correo o tu calendario, lo metes en una VM o en un contenedor. Las VMs de macOS te dan snapshots, portabilidad entre Macs, límites de memoria y disco, y la posibilidad de tirarla y recrearla en un segundo. El precio era una inferencia inutilizable. Con Cua, ese precio baja a un 1-5% de overhead. El caso de uso "agente always-on dentro de una VM que revisa backups, code reviews y triage de correos" pasa de ser teóricamente bueno a ser operativamente viable.
Reproducibilidad y portabilidad de benchmarks. Si tu agente corre dentro de una VM, puedes versionar la VM, moverla entre Macs (M1, M2, M3, M4) y reproducir exactamente el mismo entorno. Es un cambio cualitativo para la evaluación: en vez de "corrí este benchmark en mi Mac un martes a las tres", puedes decir "lo corrí en la misma VM, revisión 47, sobre tres Macs distintas, mismos seeds, mismos resultados". Los benchmarks de inferencia local dejan de ser evidencia anecdótica y empiezan a ser datos que puedes defender ante un equipo.
Composición con el stack agéntico que ya existe. La capa de Cua no impone qué modelo corre, qué runtime usa ni qué framework agéntico lo invoca. Llama.cpp carga la librería y el resto del stack (Ollama, vLLM en Mac, frameworks de agentes) sigue funcionando encima. En particular, la combinación con Cua Driver (el proyecto de computer-use agents del mismo equipo) es la que tiene más sentido a corto plazo: un agente que opera una interfaz gráfica, aislado en una VM, con rendimiento casi bare-metal.
Lo que sigue faltando: GPU passthrough estilo VFIO (guest con acceso directo al dispositivo físico, sin paravirtualización). Eso sigue siendo imposible en Apple Silicon a nivel de framework, y no se ve en el roadmap. Mientras tanto, esta capa es el mejor workaround disponible y resuelve el 95% del problema para inferencia de LLMs.
Cómo probarlo hoy
El repo está en github.com/trycua/cua. En blog/gpu-passthrough-macos-vms.md está la nota técnica completa, y en el mismo repo tienes los scripts de build, el probe de capabilities y los logs de benchmark. Para reproducir los números necesitas una Mac M1/M2/M3/M4 con macOS reciente y Lume para crear la VM guest. El setup todavía no es trivial, pero es el tipo de proyecto que en un par de meses va a tener un brew install y un wrapper de Ollama que lo habilite por defecto.
Mi recomendación si tienes una VM de macOS corriendo agentes: pruébalo con Gemma 4 12B Q4_0 (un tamaño parecido al de los modelos abiertos para agentes que cubrimos en Glimmer) y mídelo contra tu setup actual. Si la brecha con bare-metal te baja de un 5%, ya no hay excusa para no aislar el agente en producción.
Y si lo que te importa es exprimir hardware limitado, Swiftlet sigue siendo la lectura recomendada. Los dos proyectos apuntan a problemas opuestos del mismo espacio: hacer que los modelos locales sean usables. La combinación de los dos, en una VM, es lo más cerca que estamos de un setup de agente local production-ready sobre Apple Silicon.