ONNX Runtime en el edge: inferencia portable y rápida
Índice de contenidos
- Puntos clave
- Qué resuelve ONNX de verdad
- Export: el paso que parece fácil
- Execution Providers: donde vive el rendimiento
- Cuantización y optimización de grafo
- Browser, móvil y el caso real del edge
- Conclusión
- Preguntas frecuentes
- ¿Por qué mi modelo ONNX funciona en pruebas pero falla en producción con otro batch size?
- ¿Qué `opset_version` debo usar al exportar a ONNX?
- ¿Cuánto reduce la cuantización el tamaño y la latencia de un modelo en ONNX Runtime?
- Fuentes
Un modelo entrenado en PyTorch o TensorFlow, ejecutado igual en servidor, móvil, navegador o un gateway ARM en planta: eso es lo que resuelve ONNX Runtime. Convierte el formato ONNX en un artefacto portable de verdad, exportado una vez, a costa de ceder algo de rendimiento máximo frente a un runtime nativo específico de cada plataforma.
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. De repente tienes que servir inferencia en un servidor Linux, dentro de una app iOS, sobre un gateway industrial ARM y en una pestaña del navegador del cliente. Cada destino trae su propio runtime.
ONNX Runtime[1] es la apuesta a la que más gente llega cuando ese dolor se vuelve crónico.
Puntos clave
-
ONNX Runtime convierte el formato ONNX en herramienta utilizable: exportas una vez desde PyTorch o TensorFlow y ejecutas en casi cualquier sitio con el mismo artefacto.
-
Los Execution Providers (EPs) separan el grafo del hardware; el mismo código corre en GPU de datacenter en desarrollo y en CPU de edge en producción.
-
La cuantización dinámica es la puerta barata: una línea, sin calibración, 4× de reducción de tamaño en CNNs típicas.
-
El estado real de los EPs de NPU en el primer trimestre de 2024 va por detrás de la narrativa comercial.
-
Si el despliegue es exclusivamente GPU NVIDIA grande, TensorRT o vLLM probablemente ganen.
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 formato puente, cada nuevo target añade semanas de reingeniería: convertir el grafo, validar outputs, redescubrir operadores no soportados.
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: 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. 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 funcione. En PyTorch el elemento crítico es declarar qué ejes son dinámicos:
import torch
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model,
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. El opset_version=17 es un buen equilibrio: 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 y diferencias float32/float16.
Execution Providers: donde vive el rendimiento
La arquitectura separa el grafo del hardware mediante Execution Providers. El EP por defecto es CPU (optimizado con MLAS), pero el argumento real del runtime es la lista que se 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 en producción sin cambiar una línea. Este patrón complementa bien la automatización industrial descrita en redes 5G privadas en planta, donde los gateways ARM necesitan inferencia local sin dependencia de nube.
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:
-
Dinámica: una línea, sin calibración, el punto de partida por defecto en CNNs. Reducción 4× en tamaño.
-
Estática: con dataset de calibración representativo. Necesaria para el 2-4× completo en latencia.
En modelos grandes como Whisper o BERT, INT8 suele ser la diferencia entre correr en un móvil decente y no correr.
Browser, móvil y el caso real del edge
onnxruntime-web ejecuta modelos ONNX en el navegador usando WebGPU cuando está disponible y WebAssembly como fallback. Para image classification, detección o un Whisper-tiny transcribiendo en el cliente, desaparece la necesidad de un servicio de inferencia y su factura de GPU.
En móvil, ONNX Runtime Mobile produce .aar para Android y frameworks Swift/Objective-C para iOS. En edge embebido (Jetson, Raspberry Pi, gateways industriales ARM) el argumento es la portabilidad. Iteras en workstation con CUDA, validas en laptop con CPU y despliegas en Jetson Orin con el EP de TensorRT sin reescribir nada.
La capacidad de servir modelos cuantizados en dispositivos heterogéneos es donde ONNX Runtime resulta más valioso en proyectos que mezclan servidores, móviles y edge industrial.
Conclusión
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, todos los targets que soporta y suficiente margen de rendimiento en cada uno para que la portabilidad salga rentable.
La advertencia honesta al primer trimestre de 2024 es el estado de las NPUs: la narrativa comercial va por delante de la realidad operativa. Para todo lo demás (CPU, GPU consumer, browser, mobile clásico) ya es la elección sensata.
Preguntas frecuentes
¿Por qué mi modelo ONNX funciona en pruebas pero falla en producción con otro batch size?
Porque el export desde PyTorch no declaró dynamic_axes: sin ese argumento el modelo queda congelado al batch size del tensor de exportación, y si ese tensor tenía batch 1 rompe cuando llegan lotes distintos. Hay que pasar dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}} a torch.onnx.export. Tras exportar, alimenta el mismo tensor al modelo PyTorch y a la sesión ONNX Runtime, y compara con np.allclose a tolerancia conservadora. Las discrepancias típicas vienen de operadores custom mal traducidos y de diferencias float32/float16.
¿Qué `opset_version` debo usar al exportar a ONNX?
opset_version=17 es un buen equilibrio: suficientemente moderno para modelos recientes y suficientemente antiguo para que todos los Execution Providers lo entiendan. ONNX versiona sus operadores estándar por opset, así que un opset demasiado nuevo puede dejar fuera a EPs como OpenVINO, Core ML, NNAPI o QNN. Al cargar el modelo, ONNX Runtime aplica además pasadas automáticas de optimización (fusión de operadores, constant folding, eliminación de nodos muertos) sin que tengas que intervenir.
¿Cuánto reduce la cuantización el tamaño y la latencia de un modelo en ONNX Runtime?
La cuantización dinámica es la puerta barata: una línea, sin calibración, el punto de partida por defecto en CNNs, con una reducción de tamaño de 4×. La estática requiere un dataset de calibración representativo y es necesaria para obtener el 2-4× completo en latencia. En modelos grandes como Whisper o BERT, INT8 suele ser la diferencia entre correr en un móvil decente y no correr. Con onnxruntime-web, un Whisper-tiny puede incluso transcribir en el navegador vía WebGPU con WebAssembly como fallback.