El patrón de dos capas en Apple Silicon: código determinista barato primero, CoreML para lo pesado

| Última modificación: 4 de mayo de 2026 | Tiempo de Lectura: 10 minutos
Premios Blog KeepCoding 2025

Co-Fundador de KeepCoding

En los dos posts anteriores (sobre hashes perceptuales en el contexto del Chat Control y sobre sus colisiones adversariales) vimos cómo 40 líneas de Python resuelven un problema que luego la industria despliega a escala global. Los hashes perceptuales son el ejemplo canónico de capa barata determinista: sin modelo, sin GPU, sin dependencias pesadas.

Los hashes resuelven detección de duplicados y similitudes. Pero en un pipeline real de procesamiento de imágenes hay trabajo que no puede hacerse con algoritmos clásicos: detectar bounding boxes de un watermark arbitrario, rellenar el hueco con inpainting que respete el contexto visual, clasificar si una cara es adulta, extraer atributos semánticos. Ese trabajo requiere redes neuronales grandes.

La pregunta que resuelve este post: cómo desplegar esas redes en Apple Silicon sin depender de APIs cloud, sin pagar tokens por imagen, y aprovechando el Apple Neural Engine. Respuesta corta: PyTorch → ONNX → CoreML, en tres comandos. Respuesta larga: el valor real no es técnico. Es arquitectónico. El patrón que hace que la cosa funcione lo llevo aplicando en cosas aparentemente no relacionadas —análisis de sentimiento, moderación de imágenes— y es el mismo.

El patrón: dos capas, una filosofía

Hace unas semanas publiqué SentimentKit, una librería para análisis de sentimiento que surgió de descubrir que NLTagger de Apple puntúa «borra el fichero temporal» como -0.8 (muy negativo). El problema no era específico de Apple: es una clase entera de sesgos sistemáticos en modelos de ML pre-entrenados.

La solución no fue «buscar un modelo mejor». Fue arquitectónica. SentimentKit tiene cuatro capas en pipeline:

  1. Detector de keywords deterministas. Diccionarios curados de profanidad, frustración y expresiones positivas en 8 idiomas. ~20 KB. Corre primero.
  2. NLTagger de Apple (con corrección de sesgo técnico). Si el mensaje no disparó nada en la capa 1.
  3. Modelo específico de dominio (reglas + heurísticas para código/técnico).
  4. Foundation Models (LLM local de Apple) solo si las tres anteriores devolvieron ambigüedad.

El patrón: la capa determinista barata corre primero, y el ML solo interviene cuando las capas cheap no pueden decidir. El 70-80% del tráfico se resuelve en la capa 1 sin tocar un modelo. El coste energético, la latencia y el riesgo de alucinación bajan proporcionalmente.

Cuando empecé a diseñar un pipeline de limpieza de imágenes a escala semanas después, descubrí que estaba aplicando el mismo patrón sin pensarlo:

  1. Validación determinista (Pillow verify, magic bytes, límites de tamaño). Descarta lo obvio.
  2. Hashes perceptuales (aHash + dHash + pHash). Agrupa duplicados por ~10-15 ms por imagen.
  3. OCR clásico con regex de tokens conocidos. Marca watermarks por texto.
  4. Redes neuronales (YOLOv11 para detección, LaMa para inpainting). Solo sobre las canónicas supervivientes, que son ~10-20% del volumen inicial.

No es casualidad. Es la misma filosofía aplicada a otro dominio. Y la pregunta operativa —ejecutada mil veces en producción— es la misma: cómo hacer que las capas neuronales corran lo más barato posible, sin pagar APIs cloud y sin arrastrar infraestructura de GPU propia.

La respuesta, en Apple Silicon, es ANE vía CoreML. El puente entre PyTorch (donde los modelos nacen) y CoreML (donde los desplegamos) se llama ONNX.

ONNX: el lenguaje intermedio que nadie te explicó

ONNX (Open Neural Network Exchange) es un formato de intercambio para modelos de redes neuronales. Lo mantiene una fundación con Microsoft, Meta, NVIDIA y otros. La especificación describe cómo serializar el grafo computacional de un modelo —las operaciones, sus conexiones, los pesos— en un archivo portable.

Por qué importa: desacopla el entrenamiento del despliegue.

Antes de ONNX, si entrenabas un modelo en PyTorch, estabas casado con PyTorch en producción. Si querías moverlo a TensorFlow Serving, o a CoreML, o a un runtime embebido, tenías que reimplementarlo operación por operación. El proceso era manual, propenso a errores, y mantenido por separado para cada combinación.

ONNX introduce un pivote. El flujo pasa a ser:

PyTorch (entrenamiento) → ONNX (intercambio) → Runtime X (producción)

Donde Runtime X puede ser: onnxruntime (multi-backend), TensorRT (NVIDIA), OpenVINO (Intel), CoreML (Apple), TFLite (móvil), Triton (servidor), WebAssembly (navegador), etc.

El modelo se exporta una vez; el runtime elige el backend óptimo para el hardware disponible. En Apple Silicon, el backend que queremos es CoreML Execution Provider, que a su vez planifica sobre CPU, GPU Metal y Apple Neural Engine según la operación.

Limitaciones honestas

No todas las operaciones de PyTorch tienen equivalente directo en ONNX. Las arquitecturas modernas usan custom ops (atención flash, convoluciones exóticas) que a veces no exportan limpiamente. La solución práctica: si tu modelo es un YOLO, un ResNet, un Transformer estándar o un U-Net, exporta sin drama. Si tu modelo tiene operaciones raras, revisa la matriz de compatibilidad antes de asumir que exporta.

Para el caso que nos ocupa —YOLOv11 fine-tuneado y LaMa— ambos tienen exportación ONNX de primera clase. Para YOLOv11, ultralytics lo hace en una línea. Para LaMa, el equipo de Carve AI ya publicó una versión ONNX en HuggingFace; ni siquiera hay que exportar.

CoreML: el runtime de Apple y el Neural Engine

CoreML es el runtime de machine learning de iOS y macOS. Existe desde iOS 11 (2017). Desde la llegada del chip A11 Bionic (iPhone 8, 2017) y todos los Apple Silicon posteriores, CoreML puede ejecutar modelos en tres unidades distintas dentro del mismo SoC:

  • CPU. Funciona siempre, útil para operaciones no paralelizables o modelos muy pequeños.
  • GPU Metal. El motor de gráficos también hace cómputo general. Muy rápido en batches grandes, ideal para training o inference masiva.
  • Apple Neural Engine (ANE). Coprocesador específico para tensor ops. Diseñado para inference single-shot en bajo consumo. 16 núcleos en los M-series, ~18 TOPS en M4. Unidades optimizadas para convoluciones, matmul e inference en precisión reducida (FP16, INT8).

CoreML decide por ti dónde corre cada operación. En la práctica: operaciones compatibles con ANE → ANE; el resto → GPU o CPU. El desarrollador no configura el scheduler; CoreML optimiza.

La ventaja clave de ANE sobre GPU Metal: eficiencia energética. En inference single-shot, ANE consume aproximadamente 3-5 veces menos energía que GPU Metal para el mismo trabajo, según benchmarks públicos de la propia Apple y de terceros (Geekbench ML, CreateML). En un MacBook Pro con batería, la diferencia es la que separa «correr el pipeline una vez» de «poder correr el pipeline cada noche sin drenar el portátil».

La desventaja: ANE no acelera entrenamiento, y no todas las operaciones están soportadas. Las operaciones que caen en CPU como fallback deterioran la ventaja energética. Los exports bien hechos (modelos cuantizados a FP16/INT8, arquitecturas estándar) usan ANE al 90%+ de operaciones.

El rol de onnxruntime-coreml

El onnxruntime de Microsoft tiene una capa de Execution Providers que abstrae el hardware. Instalando el paquete onnxruntime-coreml (disponible desde 2022), el runtime usa CoreML como backend transparentemente:

import onnxruntime as ort

session = ort.InferenceSession(
    "yolo11x-watermark.onnx",
    providers=["CoreMLExecutionProvider", "CPUExecutionProvider"],
)

output = session.run(None, {"images": input_tensor})

Esas tres líneas son todo lo que hace falta para que un modelo ONNX corra en Apple Neural Engine. El fallback a CPU está listado segundo para operaciones no soportadas. No hay que escribir código Swift, no hay que usar Xcode, no hay que empaquetar .mlmodel. Solo Python + ONNX + onnxruntime-coreml.

La conversión en tres comandos reales

Voy a mostrar el flujo completo para un caso que tengo en producción: YOLOv11 fine-tuneado para detección de watermarks.

Paso 1: obtener el modelo PyTorch

Para YOLOv11, uso corzent/yolo11x_watermark_detection — modelo YOLOv11-extra fine-tuneado sobre un dataset curado de watermarks, 114 MB, licencia MIT:

hf download corzent/yolo11x_watermark_detection --local-dir ./models

Esto baja el best.pt (el modelo PyTorch) más metadata. No requiere token de HuggingFace para este repositorio público.

Paso 2: exportar a ONNX

ultralytics (la librería oficial de YOLOv11) lo hace en una llamada:

from ultralytics import YOLO

model = YOLO("./models/best.pt")
model.export(format="onnx", imgsz=640, simplify=True)
# -> ./models/best.onnx (228 MB)

El flag simplify=True colapsa operaciones redundantes y mejora compatibilidad con ANE. Tiempo medido en un M3: unos 2-3 segundos. Requiere tener onnx y onnxslim como dependencias, aparte de ultralytics y torch.

Paso 3: inferencia con CoreML Execution Provider

import onnxruntime as ort
import numpy as np
from PIL import Image

session = ort.InferenceSession(
    "./models/best.onnx",
    providers=["CoreMLExecutionProvider", "CPUExecutionProvider"],
)

img = Image.open("photo.jpg").resize((640, 640))
tensor = np.asarray(img, dtype=np.float32).transpose(2, 0, 1)[None] / 255.0

outputs = session.run(None, {"images": tensor})
# outputs[0] shape: (1, 5, 8400) — cx, cy, w, h, score por detección

Al cargar la sesión, onnxruntime imprime un log ilustrativo del reparto:

CoreMLExecutionProvider::GetCapability
  number of partitions supported by CoreML: 7
  number of nodes in the graph: 617
  number of nodes supported by CoreML: 609

Es decir: 609 de 617 operaciones (98.7%) corren por CoreML —y de ahí a ANE— y solo 8 fallback a CPU. El grafo se parte en 7 subgrafos porque las operaciones CPU se intercalan en puntos específicos del pipeline.

Tiempo medido en un M3 (post-warmup): 42-48 ms por imagen. La primera ejecución paga un coste de compilación del grafo CoreML de ~55 ms que se amortiza a partir de la segunda imagen.

Paso opcional: conversión a formato nativo .mlpackage

Si el objetivo es empaquetar el modelo dentro de una app iOS/macOS nativa (no correrlo desde Python), haría falta convertir a .mlpackage. Aquí llega la fricción real del ecosistema en 2026.

Los dos caminos que vienen a la cabeza:

(a) PyTorch → .mlpackage directo vía ultralytics.export(format="coreml"). Internamente pasa por coremltools y el converter PyTorch. Lo probé en el mismo experimento que montó todo lo anterior: rompe en Python 3.14 + torch 2.11 + coremltools 9 con TypeError: only 0-dimensional arrays can be converted to Python scalars en operaciones aten::Int. Es un pin del ecosistema, no un fallo del modelo.

(b) ONNX → .mlpackage vía coremltools.convert(onnx_file). Funcionaba en coremltools ≤6. Pero coremltools 9 eliminó el ONNX converter. Al pasarle un archivo .onnx devuelve: «source framework not detected, choose from tensorflow / pytorch / milinternal». ONNX no está en la lista.

El camino pragmático en 2026 —si realmente necesitas .mlpackage— es un venv aislado con Python 3.11, torch 2.3 y coremltools 6.x exclusivamente para la conversión. Una vez, no recurrente. Luego empaquetas el .mlpackage en tu app.

Pero la mayoría de pipelines no necesitan .mlpackage. Si tu consumidor del modelo es un script Python corriendo en Mac (batch processing, pipeline nocturno, herramienta CLI, servidor interno), onnxruntime-coreml cubre el caso sin conversión adicional. Solo necesitas .mlpackage si vas a distribuir el modelo dentro de una app iOS/macOS publicada en la App Store.

Detecciones reales por tipo de marca

Para verificar que el pipeline no es solo un experimento de juguete, ejecuté el YOLO exportado contra muestras reales que cubren los cinco patrones de watermark más comunes que uno se encuentra en la web. Cuatro imágenes por patrón, veinte en total. Las detecciones por imagen son post-NMS (agrupación de bounding boxes solapadas):

Patrón de watermark Detecciones/img Confianza max Cobertura Resultado
Banner inferior grande con texto 3-5 0.76-0.87 7-15% ✅ detectado limpio
Mosaico tiled por toda la imagen 5 0.73-0.81 3-4% ✅ mosaic agrupado
Texto central grande 1-2 0.25-0.67 6-12% ⚠️ detectado, umbral justo
Logo de dominio en esquina 0 0.00 0% no detectado
Texto lateral pequeño 0-1 0.00-0.71 0-1% ❌ mayormente no detectado

El modelo —entrenado sobre un dataset genérico de watermarks intrusivos— detecta bien las marcas visualmente grandes (banners, mosaicos, texto central) y falla en las marcas de texto pequeñas de esquina (logos de dominio, texto lateral). No es un fallo de implementación ni del export ONNX: es una limitación del modelo para ese tipo de marca. Encaja con la intuición: un YOLO genérico está entrenado para bounding boxes de objetos salientes, no para texto fino de 15 píxeles.

La consecuencia de diseño es limpia y útil: YOLO y OCR-based detection son complementarios, no alternativos. Para un pipeline completo de detección de watermarks hacen falta ambas capas:

  • OCR con regex de tokens conocidos (Apple Vision VNRecognizeTextRequest en Mac, PaddleOCR como fallback) detecta dominios, teléfonos, URLs y cualquier marca textual de cualquier tamaño. Barato, determinista, CPU.
  • YOLO detecta marcas visualmente grandes y no textuales (mosaicos tiled, banners de banda). Más caro por imagen pero corre en ANE a ~45 ms.

La lección generalizable: no asumas que una red neuronal moderna cubre todos los casos del dominio; mide en tus datos y ensambla. La tentación de poner un único modelo grande al principio del pipeline —un CLIP, un LLM multimodal, un YOLO-world de propósito general— se paga con cobertura dispareja por subtipo y sin manera barata de diagnosticarlo. Dos capas especializadas son más robustas que una grande y opaca.

Tiempos medidos sobre 20 imágenes reales (M3, post-warmup): 42-48 ms por imagen, consistentes con la predicción del bench sintético. El warmup de CoreML (compilación del grafo) es ~55 ms la primera llamada.

Por qué este camino es mejor que PyTorch-MPS

En la comunidad Python existe la tentación de usar device="mps" de PyTorch y llamarlo día. Funciona. Es lo que hacen la mayoría de los notebooks y tutoriales de YOLOv11 en Apple Silicon. Pero tiene tres desventajas operativas:

  1. No usa ANE. PyTorch-MPS va a GPU Metal. El Neural Engine queda inactivo. El consumo energético es 3-5x mayor para el mismo trabajo.
  2. Dependencias pesadas. PyTorch completo son ~5 GB de instalación. onnxruntime-coreml son ~100 MB. En un CI o en un entorno reproducible (Docker multi-plataforma, por ejemplo), la diferencia importa.
  3. Python 3.13 bloqueante en algunos modelos. Herramientas como simple-lama-inpainting tienen dependencias (pillow==9.5.0) que solo compilan hasta Python 3.13. En un monorepo con Python 3.14, introducir un venv aislado por cada modelo es fricción operativa. Con ONNX, el modelo corre bajo el Python que tengas, sin pins de PIL.

La regla que sigo: si el modelo va a ejecutar en producción (server-side, batch, cron, lo que sea), exporta a ONNX y usa onnxruntime-coreml. Reserva PyTorch para investigación, fine-tuning y exploración. La fricción de convertir a ONNX se paga una vez; los beneficios (eficiencia, portabilidad, menor superficie de dependencias) se acumulan cada vez que el modelo corre.

Volviendo al patrón: el pipeline completo en Apple Silicon

Con las piezas ensambladas, el pipeline de limpieza de imágenes del que partimos queda así:

Capa Tecnología Coste por imagen % volumen que llega aquí
1. Validación Pillow magic bytes ~1 ms CPU 100%
2. Hashes perceptuales 40 líneas numpy ~10 ms CPU 99%
3. NudeNet (explicit filter) ONNX + ANE ~200 ms ANE 98%
4. OCR watermarks Apple Vision local ~50 ms CPU 95%
5. Clustering dedup UnionFind sobre hashes ~5 ms CPU 95%
6. Quality scoring numpy puro ~15 ms CPU ~20% (canonicals)
7. YOLOv11 detection ONNX + ANE ~40 ms ANE ~10% (canonicals con watermark)
8. LaMa inpainting ONNX + ANE ~400 ms ANE ~10%
9. CV attributes (externo) Qwen VL via OpenRouter ~$0.00015 + 3s ~20%

El 80% del volumen se resuelve en capas con coste sub-20ms y cero consumo remoto. Las redes neuronales —cuando aparecen— corren en ANE con coste energético bajo. Y solo el ~20% final se va a un API externo, lo cual baja la factura de Qwen de ~$3.50 por pase completo a ~$0.50-0.80.

Es el mismo perfil económico que SentimentKit aplicado a imágenes. Mismo patrón, dominio distinto, mismos beneficios:

  • Latencia percibida muy baja para el caso común.
  • Consumo energético dominado por operaciones de CPU y ANE, no GPU.
  • Superficie de dependencias manejable.
  • Fallo del ML (alucinación, timeout del API) degrada el sistema solo para el ~20% final, no para el 100%.

Cierre: arquitectura, no tecnología

La lección no es «usa CoreML», ni «exporta a ONNX», ni «odia PyTorch-MPS». Las herramientas son intercambiables.

La lección es el patrón: determinismo primero, ML después.

Cuando diseñes un pipeline —sentimiento, visión, moderación, recomendación, cualquier cosa que parezca pedir un LLM al principio— pregúntate qué parte se puede resolver con reglas, regex, hashes, lookups en diccionarios. Esa parte corre primera, gratis, determinista y auditable. Lo que sobreviva sube al siguiente escalón.

En Apple Silicon, el siguiente escalón es CoreML vía ONNX. Es la combinación más eficiente operativamente disponible fuera de un datacenter. Si vas a desplegar un modelo en producción en Mac o iOS, el camino PyTorch → ONNX → CoreML es el camino correcto.

Y si vas a diseñar tu próxima librería, ten presente lo que he comprobado en dos dominios distintos: la arquitectura de dos capas no es una optimización temprana. Es el modelo mental por defecto. El error es pensarlo al revés —meter el modelo pesado primero y añadir reglas a posteriori cuando la factura empieza a doler—.


Referencias

  • ONNX specification. Open Neural Network Exchange, GitHub: onnx/onnx.
  • Microsoft. onnxruntime CoreML Execution Provider documentation.
  • Apple. Core ML Framework documentation, Apple Developer.
  • Apple. Apple Neural Engine — hardware specifications, developer.apple.com.
  • Ultralytics. YOLOv11 export documentation, docs.ultralytics.com/modes/export/.
  • Apple. coremltools project, apple.github.io/coremltools/.
  • Carve AI. LaMa-ONNX, Hugging Face: Carve/LaMa-ONNX.
  • fancyfeast. joycaption-watermark-detection, Hugging Face.
  • Pérez, F. «El análisis de sentimiento de Apple cree que ‘borra el fichero temporal’ es una amenaza de muerte», frr.dev 2026-04-05.
  • Pérez, F. «64 bits deciden qué puedes subir a internet», frr.dev 2026-04-18.
  • SentimentKit, librería open-source con el patrón de cuatro capas.

Read this article in English.

Noticias recientes del mundo tech


¡CONVOCATORIA ABIERTA!

Desarrollo de apps móviles ios & Android

Full Stack Bootcamp

Clases en Directo | Profesores en Activo | Temario 100% actualizado

Descárgate también el informe de tendencias en el mercado laboral 2026.

Fórmate con planes adaptados a tus objetivos y logra resultados en tiempo récord.
KeepCoding Bootcamps
Resumen de privacidad

Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.