Desplegar un modelo de machine learning fuera del notebook donde se entrenó suele ser donde la fantasía se rompe. Entrenas en PyTorch con una GPU en la nube, y de repente tienes que servir la inferencia en un servidor Linux, dentro de una app iOS, sobre un gateway industrial ARM y —si el equipo de producto se pone creativo— en una pestaña del navegador del cliente. Cada destino tradicionalmente trae su propio runtime: TensorFlow Serving aquí, Core ML allá, TensorFlow Lite para Android, TensorFlow.js para el browser. Con suerte, el mismo modelo; con mucha suerte, la misma precisión numérica.
ONNX Runtime —el motor de inferencia multiplataforma impulsado por Microsoft— es la apuesta a la que más gente llega cuando ese dolor se vuelve crónico. Convierte ONNX (Open Neural Network Exchange) de especificación en herramienta utilizable: exportas una vez desde PyTorch o TensorFlow y ejecutas en casi cualquier sitio con el mismo artefacto. En la versión 1.17, actual a marzo de 2024, ya es suficientemente maduro como para ser default razonable en la mayoría de despliegues edge. Pero no es gratis, y conviene entender qué se gana y qué se paga.
Qué resuelve ONNX de verdad
El problema no es técnico puro, es organizativo. Un equipo de ML que entrena en PyTorch y un equipo de mobile que integra en iOS hablan lenguajes distintos. Sin un formato puente, cada nuevo target añade semanas de reingeniería: convertir el grafo, validar que los outputs coinciden dentro de tolerancia, redescubrir qué operadores no están soportados, reescribir la pipeline de preprocessing.
ONNX corta ese nudo proponiendo un formato intermedio abierto —un grafo computacional con operadores estándar versionados por opset—. ONNX Runtime es la implementación de referencia que sabe ejecutarlo: un artefacto .onnx que vale para servidor, móvil, browser y edge sin multiplicar el tooling.
Si el equipo vive exclusivamente en un ecosistema, la alternativa nativa suele rendir más. TensorRT saca más de una GPU NVIDIA; Core ML exprime mejor un A17 Pro; TFLite con XNNPACK sigue muy competitivo en Android. Pero en cuanto aparece el segundo target, el coste de mantener dos o tres runtimes nativos supera lo que se perdió eligiendo el denominador común.
Export, el paso que parece fácil
Todo el valor de ONNX Runtime depende de que el export desde el framework original funcione. En PyTorch esto significa torch.onnx.export con una entrada de ejemplo, nombres de tensores y —crítico— declaración explícita de qué ejes son dinámicos:
import torch
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model.eval(),
dummy_input,
"model.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch_size"},
"output": {0: "batch_size"},
},
opset_version=17,
)
Sin dynamic_axes, el modelo queda congelado a batch size 1 y rompe en producción al recibir lotes distintos. El opset_version determina qué operadores están disponibles; 17 es un buen equilibrio a principios de 2024: suficientemente moderno para modelos recientes y suficientemente antiguo para que todos los EPs lo entiendan.
El paso no-negociable después del export es la validación: alimentar el mismo tensor al modelo PyTorch y a la sesión ONNX Runtime y comparar con np.allclose a tolerancia conservadora. Fuentes típicas de discrepancia: operadores custom mal traducidos, diferencias de precisión float32/float16 y el eterno batch normalization entre modo training e inference.
Execution Providers: donde vive el rendimiento
La arquitectura de ONNX Runtime separa el grafo del hardware mediante Execution Providers (EPs). El EP por defecto es CPU —optimizado con MLAS—, pero el argumento real del runtime es la lista que se le añade encima: CUDA y TensorRT para NVIDIA, OpenVINO para Intel, DirectML para Windows con cualquier GPU, ROCm para AMD en Linux, Core ML para Apple Silicon, NNAPI para Android, QNN para Snapdragon, WebGPU y WebAssembly para el navegador.
La configuración es una lista priorizada: el runtime intenta el primero, cae al siguiente si el hardware no está disponible y termina en CPU como red de seguridad. El mismo código Python hace inferencia en GPU de datacenter en desarrollo y en CPU del edge device en producción sin cambiar una línea.
El matiz importante en marzo de 2024 es el estado real de los EPs de NPU. QNN para los Snapdragon 8 Gen 3 existe y funciona pero requiere conversión adicional y la cobertura de operadores sigue incompleta. Los NPUs de los Intel Core Ultra (Meteor Lake) se tocan vía OpenVINO y las demos prometen, pero la documentación va por detrás del silicio. El Neural Engine de Apple solo se alcanza vía Core ML, que ONNX Runtime invoca indirectamente cuando delega el subgrafo. Están ahí, pero tratarlas como aceleradores transparentes todavía es optimista.
Cuantización y optimización de grafo
Al cargar el modelo, ONNX Runtime aplica pasadas automáticas —fusión de operadores, constant folding, eliminación de nodos muertos— sin intervención. El salto grande en tamaño y latencia viene de la cuantización, que reduce pesos y activaciones de float32 a INT8 (o INT4 para transformers) con pérdida de calidad normalmente por debajo del 1%.
La cuantización dinámica es la puerta barata: una línea, sin calibración, aceptable para la mayoría de CNNs. Para el 4× en tamaño y 2-4× en latencia completos, lo correcto es cuantización estática con un dataset de calibración representativo. En modelos grandes como Whisper o BERT, INT8 es muchas veces la diferencia entre correr en un móvil decente y no correr.
Browser, móvil y el caso real del edge
onnxruntime-web es la pieza infravalorada del catálogo. Ejecuta modelos ONNX en el navegador usando WebGPU cuando está disponible y WebAssembly como fallback. Para image classification, detección, o incluso un Whisper-tiny transcribiendo en el cliente, desaparece la necesidad de un servicio de inferencia y con él la factura de GPU.
En móvil, ONNX Runtime Mobile produce .aar para Android y frameworks Swift/Objective-C para iOS, con bindings de React Native y plugins de Flutter razonablemente mantenidos. Para modelos de 20-100 MB es casi siempre la opción con menos fricción. En edge embebido —Jetson, Raspberry Pi, gateways industriales ARM— el argumento es portabilidad: iterar en workstation con CUDA, validar en laptop con CPU y desplegar en Jetson Orin con el EP de TensorRT sin reescribir nada.
Mi lectura
ONNX Runtime no es el motor más rápido en ninguna plataforma concreta y casi ningún benchmark puntual lo convertirá en campeón. No lo pretende. Su propuesta es absorber la complejidad de la heterogeneidad: un artefacto, un API, muchos targets y suficiente margen de rendimiento en cada uno para que la portabilidad salga rentable.
El cálculo es claro cuando hay que servir en más de una plataforma o cuando no se quiere depender del runtime propietario de un proveedor cloud. Si el despliegue es exclusivamente GPU NVIDIA grande en datacenter, TensorRT o vLLM probablemente ganen. Si es exclusivamente iOS, Core ML nativo rinde mejor. En cuanto aparece el segundo target —y con NPUs de PC, browser inference y apps móviles empujando en esa dirección, cada vez pasa antes— el trade-off se inclina solo.
La advertencia honesta a marzo de 2024 es el estado de las NPUs: la narrativa comercial va por delante de la realidad operativa, y quien apueste a que ONNX Runtime le va a abstraer mágicamente el Hexagon o el Neural Engine chocará con operadores no soportados y cobertura parcial. La promesa se va cumpliendo, pero a ritmo de silicio, no de tuit. Para todo lo demás —CPU, GPU consumer, browser, mobile clásico— ya es la elección sensata.