YOLO Vision 2026:

Cómo exportar modelos PyTorch que no son YOLO con Ultralytics#

Ultralytics incluye utilidades de exportación independientes bajo ultralytics.utils.export que encapsulan múltiples motores de inferencia detrás de una interfaz coherente. Puedes exportar cualquier torch.nn.Module, incluidos modelos de imágenes de timm, clasificadores y detectores de torchvision o tus propias arquitecturas personalizadas, a ONNX, TorchScript, OpenVINO, CoreML, NCNN, PaddlePaddle, MNN, ExecuTorch, Core AI, TensorFlow SavedModel y TensorFlow Frozen Graph sin necesidad de aprender cada motor de inferencia por separado.

Desplegar modelos PyTorch en producción normalmente implica manejar un exportador distinto para cada destino: torch.onnx.export para ONNX, coremltools para dispositivos Apple, onnx2tf para TensorFlow, pnnx para NCNN, etc. Cada herramienta tiene su propia API, particularidades de dependencias y convenciones de salida. Estas utilidades lo unifican todo en un único patrón de llamada.

¿Por qué usar Ultralytics para exportar modelos que no son YOLO?#

  • Una única API en 11 formatos: aprende una sola convención de llamada en lugar de una docena.
  • Interfaz de utilidades compartida: los helpers de exportación están en ultralytics.utils.export, así que, una vez instalados los paquetes de los backends, puedes mantener el mismo patrón de llamada en todos los formatos.
  • La misma ruta de código que las exportaciones YOLO: los mismos helpers impulsan todas las exportaciones YOLO de Ultralytics.
  • Cuantización FP16 e INT8 integrada para los formatos que la admiten (OpenVINO, CoreML y MNN; solo FP16 para NCNN y Core AI).
  • Funciona en CPU: no se requiere ninguna GPU para el paso de exportación en sí, por lo que puedes ejecutarlo localmente en un portátil; la exportación a CoreML no es compatible con Windows, y la exportación a Core AI requiere macOS 26 o posterior en silicio de Apple.

Inicio rápido#

La forma más rápida es exportar a ONNX en dos líneas, sin código YOLO y sin más configuración que pip install ultralytics onnx timm:

import timm
import torch

from ultralytics.utils.export import torch2onnx

model = timm.create_model("resnet18", pretrained=True).eval()
torch2onnx(model, torch.randn(1, 3, 224, 224), output_file="resnet18.onnx")

Formatos de exportación compatibles#

Las funciones de torch2* reciben un torch.nn.Module estándar y un tensor de entrada de ejemplo. MNN, TF SavedModel y TF Frozen Graph pasan por un artefacto intermedio ONNX o Keras. En ningún caso se requieren atributos específicos de YOLO.

FormatoFunciónInstalaciónSalida
ONNXtorch2onnx()pip install onnxArchivo .onnx
TorchScripttorch2torchscript()incluido con PyTorchArchivo .torchscript
OpenVINOtorch2openvino()pip install openvinoDirectorio _openvino_model/
CoreMLtorch2coreml()pip install coremltools.mlpackage
TF SavedModelonnx2saved_model()consulta los requisitos detallados más abajoDirectorio _saved_model/
TF Frozen Graphkeras2pb()consulta los requisitos detallados más abajoArchivo .pb
NCNNtorch2ncnn()pip install ncnn pnnxDirectorio _ncnn_model/
MNNonnx2mnn()pip install MNNArchivo .mnn
PaddlePaddletorch2paddle()pip install paddlepaddle x2paddleDirectorio _paddle_model/
ExecuTorchtorch2executorch()pip install executorchDirectorio _executorch_model/
Core AItorch2coreai()pip install coreai-torch (macOS 26 o posterior en silicio de Apple)Directorio .aimodel
ONNX como formato intermedio

Las exportaciones de MNN, TF SavedModel y TF Frozen Graph pasan por ONNX como paso intermedio. Exporta primero a ONNX y después convierte.

Integración de metadatos

Varias funciones de exportación aceptan un diccionario opcional metadata (por ejemplo, torch2torchscript(..., metadata={"author": "me"})) que integra pares clave-valor personalizados en el artefacto exportado cuando el formato lo admite.

Ejemplos paso a paso#

Todos los ejemplos siguientes utilizan la misma configuración: un ResNet-18 preentrenado de timm en modo de evaluación:

import timm
import torch

model = timm.create_model("resnet18", pretrained=True).eval()
im = torch.randn(1, 3, 224, 224)
Llama siempre a `model.eval()` antes de exportar

Dropout, la normalización por lotes y otras capas que solo se usan durante el entrenamiento se comportan de forma distinta durante la inferencia. Si omites .eval(), las exportaciones producirán salidas incorrectas.

Exportar a ONNX#

from ultralytics.utils.export import torch2onnx

torch2onnx(model, im, output_file="resnet18.onnx")

Para usar un tamaño de lote dinámico, pasa un diccionario dynamic:

torch2onnx(model, im, output_file="resnet18_dyn.onnx", dynamic={"images": {0: "batch_size"}})

El opset predeterminado es 14 y el nombre de entrada predeterminado es "images". Puedes sobrescribirlos con los argumentos opset, input_names o output_names.

Exportar a TorchScript#

No se necesitan dependencias adicionales. Utiliza torch.jit.trace internamente.

from ultralytics.utils.export import torch2torchscript

torch2torchscript(model, im, output_file="resnet18.torchscript")

Exportar a OpenVINO#

from ultralytics.utils.export import torch2openvino

ov_model = torch2openvino(model, im, output_dir="resnet18_openvino_model")

El directorio contiene un par de archivos de nombre fijo, model.xml y model.bin:

resnet18_openvino_model/
├── model.xml
└── model.bin

Pasa dynamic=True para usar formas de entrada dinámicas, quantize=16 para FP16 o quantize=8 para la cuantización INT8. INT8 también requiere un argumento calibration_dataset.

Requiere openvino>=2024.0.0 (o >=2025.2.0 en macOS 15.4 o posterior) y torch>=2.1.

Exportar a CoreML#

import coremltools as ct

from ultralytics.utils.export import torch2coreml

inputs = [ct.TensorType("input", shape=(1, 3, 224, 224))]
ct_model = torch2coreml(model, inputs, im, classifier_names=None, output_file="resnet18.mlpackage")

Para modelos de clasificación, pasa una lista de nombres de clases a classifier_names para añadir una cabeza de clasificación al modelo CoreML.

Requiere coremltools>=9.0, torch>=1.11 y numpy<=2.3.5. No es compatible con Windows.

Error `BlobWriter not loaded`

coremltools>=9.0 incluye wheels para Python 3.10–3.13 en macOS y Linux. En versiones más recientes de Python, la extensión nativa no se puede cargar. Usa Python 3.10–3.13 para exportar a CoreML.

Exportar a TensorFlow SavedModel#

La exportación a TF SavedModel pasa por ONNX como paso intermedio:

from ultralytics.utils.export import onnx2saved_model, torch2onnx

torch2onnx(model, im, output_file="resnet18.onnx")
keras_model = onnx2saved_model("resnet18.onnx", output_dir="resnet18_saved_model")

La función devuelve un modelo Keras y también genera archivos LiteRT FP32 y FP16 (.tflite) dentro del directorio de salida:

resnet18_saved_model/
├── saved_model.pb
├── variables/
├── assets/
├── fingerprint.pb
├── resnet18_float32.tflite
└── resnet18_float16.tflite

Pasa quantize=8 para añadir un .tflite INT8 junto a ellos.

Requisitos:

  • tensorflow>=2.0.0,<=2.19.0
  • onnx2tf>=1.26.3,<1.29.0
  • tf_keras<=2.19.0
  • sng4onnx>=1.0.1
  • onnx_graphsurgeon>=0.3.26
  • ai-edge-litert>=1.2.0,<1.4.0 en macOS (ai-edge-litert>=1.2.0 en el resto de plataformas)
  • onnxslim>=0.1.82
  • onnx>=1.12.0,<2.0.0
  • protobuf>=5

Exportar a TensorFlow Frozen Graph#

A partir de la exportación a SavedModel anterior, convierte el keras_model devuelto en un grafo .pb congelado:

from pathlib import Path

from ultralytics.utils.export import keras2pb

keras2pb(keras_model, output_file=Path("resnet18_saved_model/resnet18.pb"))

Exportar a NCNN#

from ultralytics.utils.export import torch2ncnn

torch2ncnn(model, im, output_dir="resnet18_ncnn_model")

El directorio contiene archivos param y bin de nombre fijo junto con un wrapper de Python:

resnet18_ncnn_model/
├── model.ncnn.param
├── model.ncnn.bin
└── model_ncnn.py

torch2ncnn() comprueba la presencia de ncnn y pnnx en el primer uso.

Exportar a MNN#

La exportación a MNN requiere un archivo ONNX como entrada. Exporta primero a ONNX y después convierte:

from ultralytics.utils.export import onnx2mnn, torch2onnx

torch2onnx(model, im, output_file="resnet18.onnx")
onnx2mnn("resnet18.onnx", output_file="resnet18.mnn")

Admite quantize=16 para FP16 y quantize=8 para la cuantización INT8. Requiere MNN>=2.9.6 y torch>=1.10.

Exportar a PaddlePaddle#

from ultralytics.utils.export import torch2paddle

torch2paddle(model, im, output_dir="resnet18_paddle_model")

El directorio contiene el modelo de PaddlePaddle y los archivos de parámetros:

resnet18_paddle_model/
├── model.pdmodel
└── model.pdiparams

Requiere x2paddle y la distribución correcta de PaddlePaddle para tu plataforma:

  • paddlepaddle-gpu>=3.0.0,<3.3.0 en CUDA
  • paddlepaddle==3.0.0 en CPU ARM64
  • paddlepaddle>=3.0.0,<3.3.0 en otras CPU

No compatible con NVIDIA Jetson.

Exportar a ExecuTorch#

from ultralytics.utils.export import torch2executorch

torch2executorch(model, im, output_dir="resnet18_executorch_model")

El archivo .pte exportado se guarda dentro del directorio de salida:

resnet18_executorch_model/
└── model.pte

Requiere torch>=2.9.0 y un runtime de ExecuTorch compatible (pip install executorch). Para usarlo en el runtime, consulta la integración de ExecuTorch.

Exportar a Core AI#

from ultralytics.utils.export import torch2coreai

torch2coreai(model, im, output_file="resnet18.aimodel")

El recurso .aimodel es un directorio:

resnet18.aimodel/
├── main.mlirb
├── main.hash
└── metadata.json

La exportación se ejecuta en macOS 26 o posterior en silicio de Apple (pip install coreai-torch), y quantize=16 escribe un recurso FP16 que toma entradas de tipo float16; el recurso se ejecuta en iOS 27 y macOS 27. Consulta la integración de Core AI, incluida su nota sobre los recursos FP16 que se interrumpen al cargarse.

Verificar el modelo exportado#

Después de exportar, verifica la paridad numérica con el modelo PyTorch original antes de ponerlo en producción. Una prueba rápida con ONNXBackend de ultralytics.nn.backends compara las salidas y detecta pronto errores de trazado o cuantización:

import numpy as np
import timm
import torch

from ultralytics.nn.backends import ONNXBackend

model = timm.create_model("resnet18", pretrained=True).eval()
im = torch.randn(1, 3, 224, 224)
with torch.no_grad():
    pytorch_output = model(im).numpy()

onnx_model = ONNXBackend("resnet18.onnx", device=torch.device("cpu"))
onnx_output = onnx_model(im)[0]

diff = np.abs(pytorch_output - onnx_output).max()
print(f"Max difference: {diff:.6f}")  # ~1e-6 for an FP32 ONNX export
Diferencia esperada

La tolerancia es específica de cada formato, no global. En un ResNet-18, las exportaciones FP32 se sitúan cerca de 1e-6 para ONNX, TF SavedModel y LiteRT, y exactamente en 0 para TorchScript. NCNN es la excepción, con aproximadamente 1e-2: su runtime de CPU activa de forma predeterminada el empaquetado y la aritmética FP16, por lo que una exportación FP32 sigue ejecutándose en precisión media. Una diferencia muy superior a la línea base del formato apunta a operaciones no compatibles, una forma de entrada incorrecta o un modelo que no está en modo de evaluación. Las exportaciones FP16 e INT8 tienen tolerancias más amplias. Valida con datos reales en lugar de tensores aleatorios.

En otros runtimes, el nombre del tensor de entrada puede variar. OpenVINO, por ejemplo, utiliza el nombre del argumento forward del modelo (normalmente x para modelos genéricos), mientras que torch2onnx tiene "images" como valor predeterminado.

Ejecutar el modelo exportado#

Los modelos exportados que no son YOLO se vuelven a cargar mediante la API normal YOLO(). Las exportaciones anteriores no contienen metadatos de tarea ni del tamaño de entrada de Ultralytics, así que pasa task explícitamente y imgsz con el valor correspondiente al tensor de ejemplo utilizado en la exportación:

from ultralytics import YOLO

results = YOLO("resnet18.onnx", task="classify")("path/to/image.jpg", imgsz=224)
print(results[0].probs.top1)

imgsz es importante cuando la exportación tiene una forma de entrada fija: las exportaciones ONNX y TF SavedModel anteriores rechazan el valor predeterminado de 640. Las exportaciones TorchScript y NCNN anteriores sí aceptan otros tamaños, pero ninguno de los dos exportadores lo garantiza: ambos trazan a partir del tensor de ejemplo, por lo que un modelo que aplana los datos en una capa Linear mantiene el tamaño fijo. Comprueba tu propia exportación.

A continuación, el valor se redondea hacia arriba hasta un múltiplo del stride del modelo, que es 32 cuando no hay metadatos. Por tanto, una exportación de forma fija de 200x200 recibe una entrada de 224x224 y se rechaza aunque imgsz=200 coincida con ella. Para tamaños de entrada que no sean múltiplos de 32, llama directamente al backend.

Llamar directamente a un backend#

Para tensores sin preprocesamiento ni posprocesamiento de Ultralytics, utiliza las clases específicas de cada formato en ultralytics.nn.backends, como hace el ejemplo de verificación anterior. Cada una recibe el artefacto exportado y un dispositivo, y se puede invocar:

FormatoBackendDistribución de entrada
ONNXONNXBackendBCHW
TorchScriptTorchScriptBackendBCHW
OpenVINOOpenVINOBackendBCHW
CoreMLCoreMLBackendBHWC
TF SavedModel, Frozen GraphTensorFlowBackendBHWC
LiteRTLiteRTBackendBCHW
NCNNNCNNBackendBCHW
PaddlePaddlePaddleBackendBCHW
MNNMNNBackendBCHW
ExecuTorchExecuTorchBackendBCHW
Core AICoreAIBackendBCHW

TensorFlowBackend admite dos formatos y utiliza format="saved_model" de forma predeterminada, así que pasa format="pb" para un grafo congelado.

Tres cosas que la ruta YOLO() gestiona por ti y una llamada directa no:

  • Distribución de entrada: CoreMLBackend y TensorFlowBackend esperan BHWC. Haz primero una transposición con im.permute(0, 2, 3, 1); un tensor BCHW provoca un error de incompatibilidad de formas.
  • Autograd: envuelve las llamadas en torch.inference_mode(). TorchScriptBackend devuelve un tensor que todavía conserva un grafo de gradientes.
  • Posprocesamiento: sin metadatos, un backend deja task como None y names vacío. LiteRTBackend sigue desnormalizando cualquier salida 3D según el tamaño de la imagen, suponiendo que contiene cajas YOLO, lo cual es incorrecto para un modelo que no es YOLO con una salida 3D. Las salidas bidimensionales, como los logits de un clasificador, no se ven afectadas.

Limitaciones conocidas#

  • La compatibilidad con múltiples entradas es irregular: torch2onnx y torch2openvino aceptan una tupla o una lista de tensores de ejemplo para modelos con múltiples entradas. torch2torchscript, torch2coreml, torch2ncnn, torch2paddle, torch2executorch y torch2coreai asumen un único tensor de entrada.
  • ExecuTorch necesita flatc: el runtime de ExecuTorch requiere el compilador de FlatBuffers. Instálalo con brew install flatbuffers en macOS o apt install flatbuffers-compiler en Ubuntu.
  • No hay metadatos integrados: las exportaciones anteriores no contienen metadatos de tarea ni del tamaño de entrada de Ultralytics, por lo que YOLO() no puede inferir ninguno de los dos y necesita que pases ambos explícitamente. Consulta Ejecutar el modelo exportado.
  • Formatos exclusivos de YOLO: las exportaciones de Axelera y Sony IMX500 requieren atributos específicos de los modelos YOLO y no están disponibles para modelos genéricos.
  • Formatos específicos de plataforma: TensorRT requiere una GPU NVIDIA. RKNN requiere el SDK rknn-toolkit2 (solo Linux). Edge TPU requiere el binario edgetpu_compiler (solo Linux).

Conclusión#

Estas utilidades convierten cualquier modelo PyTorch, desde un torch.nn.Module básico hasta un artefacto ONNX, OpenVINO, CoreML, TensorFlow o de un runtime móvil listo para desplegar, mediante una única API coherente. Elige el formato que corresponda al hardware de destino, verifica la paridad numérica con el modelo original y sigue la guía de integración correspondiente para conocer los pasos de despliegue específicos del runtime.

Preguntas frecuentes#

  • Cualquier torch.nn.Module. Esto incluye modelos de timm, torchvision o cualquier modelo PyTorch personalizado. El modelo debe estar en modo de evaluación (model.eval()) antes de exportarlo. ONNX y OpenVINO también aceptan una tupla de tensores de ejemplo para modelos con múltiples entradas.

  • Todos los formatos compatibles (TorchScript, ONNX, OpenVINO, CoreML, TF SavedModel, TF Frozen Graph, NCNN, PaddlePaddle, MNN, ExecuTorch, Core AI) se pueden exportar en la CPU. No se requiere ninguna GPU para el proceso de exportación en sí. TensorRT es el único formato que requiere una GPU de NVIDIA.

  • Usa Ultralytics >=8.4.38, que incluye el módulo ultralytics.utils.export y los argumentos estandarizados output_file/output_dir.

  • Sí. Los clasificadores, detectores y modelos de segmentación de torchvision se exportan a .mlpackage mediante torch2coreml. Para los modelos de clasificación de imágenes, pasa una lista de nombres de clase a classifier_names para incorporar una cabeza de clasificación. Ejecuta la exportación en macOS o Linux. CoreML no es compatible con Windows. Consulta la integración de CoreML para obtener información sobre la implementación en iOS.

  • Sí, para varios formatos. Pasa quantize=16 para FP16 o quantize=8 para INT8 al exportar a OpenVINO, CoreML o MNN; NCNN y Core AI exportan en FP32 de forma predeterminada, aceptan quantize=16 para FP16 y no tienen ninguna ruta INT8. INT8 en OpenVINO requiere además un argumento calibration_dataset para la cuantización posterior al entrenamiento. Consulta la página de integración de cada formato para conocer las ventajas e inconvenientes de la cuantización.

  • Ejecuta el modelo PyTorch original y el modelo exportado con la misma entrada y compara las salidas. Carga el archivo exportado con el backend correspondiente (por ejemplo, ONNXBackend para ONNX) y comprueba la diferencia absoluta máxima. Evalúa la discrepancia con respecto a la referencia propia del formato. En el ejemplo de ResNet-18 anterior, FP32 ONNX, TF SavedModel y LiteRT se sitúan cerca de 1e-6, TorchScript en 0 y NCNN cerca de 1e-2 porque su entorno de ejecución en CPU usa FP16 de forma predeterminada. Una discrepancia mucho mayor apunta a operadores no compatibles, una forma de entrada incorrecta o un modelo que no está en modo de evaluación. Consulta Verifica tu modelo exportado para ver un ejemplo ejecutable.

Comentarios