How to Export Non-YOLO PyTorch Models with Ultralytics#
Ultralytics ships standalone export utilities under ultralytics.utils.export that wrap multiple backends behind one consistent interface. You can export any torch.nn.Module, including timm image models, torchvision classifiers and detectors, or your own custom architectures, to ONNX, TorchScript, OpenVINO, CoreML, NCNN, PaddlePaddle, MNN, ExecuTorch, Core AI, TensorFlow SavedModel, and TensorFlow Frozen Graph without learning each backend separately.
Deploying PyTorch models to production usually means juggling a different exporter for every target: torch.onnx.export for ONNX, coremltools for Apple devices, onnx2tf for TensorFlow, pnnx for NCNN, and so on. Each tool has its own API, dependency quirks, and output conventions. These utilities collapse that into a single calling pattern.
Why Use Ultralytics for Non-YOLO Export?#
- One API across 11 formats: learn a single calling convention instead of a dozen.
- Shared utility surface: the export helpers live under
ultralytics.utils.export, so once the backend packages are installed you can keep the same calling pattern across formats. - Same code path as YOLO exports: the same helpers power every Ultralytics YOLO export.
- FP16 and INT8 quantization built in for formats that support it (OpenVINO, CoreML, and MNN; FP16 only for NCNN and Core AI).
- Works on CPU: no GPU required for the export step itself, so you can run it locally on a laptop; CoreML export is not supported on Windows, and Core AI export needs macOS 26 or later on Apple silicon.
Quick Start#
The fastest path is a two-line export to ONNX with no YOLO code and no setup beyond 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")Supported Export Formats#
The torch2* functions take a standard torch.nn.Module and an example input tensor. MNN, TF SavedModel, and TF Frozen Graph go through an intermediate ONNX or Keras artifact. No YOLO-specific attributes are required in either case.
| Format | Function | Install | Output |
|---|---|---|---|
| ONNX | torch2onnx() | pip install onnx | .onnx file |
| TorchScript | torch2torchscript() | included with PyTorch | .torchscript file |
| OpenVINO | torch2openvino() | pip install openvino | _openvino_model/ directory |
| CoreML | torch2coreml() | pip install coremltools | .mlpackage |
| TF SavedModel | onnx2saved_model() | see detailed requirements below | _saved_model/ directory |
| TF Frozen Graph | keras2pb() | see detailed requirements below | .pb file |
| NCNN | torch2ncnn() | pip install ncnn pnnx | _ncnn_model/ directory |
| MNN | onnx2mnn() | pip install MNN | .mnn file |
| PaddlePaddle | torch2paddle() | pip install paddlepaddle x2paddle | _paddle_model/ directory |
| ExecuTorch | torch2executorch() | pip install executorch | _executorch_model/ directory |
| Core AI | torch2coreai() | pip install coreai-torch (macOS 26+ on Apple silicon) | .aimodel directory |
MNN, TF SavedModel, and TF Frozen Graph exports go through ONNX as an intermediate step. Export to ONNX first, then convert.
Several export functions accept an optional metadata dictionary (e.g., torch2torchscript(..., metadata={"author": "me"})) that embeds custom key-value pairs into the exported artifact where the format supports it.
Step-by-Step Examples#
Every example below uses the same setup, a pretrained ResNet-18 from timm in evaluation mode:
import timm
import torch
model = timm.create_model("resnet18", pretrained=True).eval()
im = torch.randn(1, 3, 224, 224)Dropout, batch normalization, and other train-only layers behave differently during inference. Skipping .eval() produces exports with incorrect outputs.
Export to ONNX#
from ultralytics.utils.export import torch2onnx
torch2onnx(model, im, output_file="resnet18.onnx")For dynamic batch size, pass a dynamic dictionary:
torch2onnx(model, im, output_file="resnet18_dyn.onnx", dynamic={"images": {0: "batch_size"}})The default opset is 14 and the default input name is "images". Override with the opset, input_names, or output_names arguments.
Export to TorchScript#
No extra dependencies needed. Uses torch.jit.trace under the hood.
from ultralytics.utils.export import torch2torchscript
torch2torchscript(model, im, output_file="resnet18.torchscript")Export to OpenVINO#
from ultralytics.utils.export import torch2openvino
ov_model = torch2openvino(model, im, output_dir="resnet18_openvino_model")The directory contains a fixed-name model.xml and model.bin pair:
resnet18_openvino_model/
├── model.xml
└── model.binPass dynamic=True for dynamic input shapes, quantize=16 for FP16, or quantize=8 for INT8 quantization. INT8 additionally requires a calibration_dataset argument.
Requires openvino>=2024.0.0 (or >=2025.2.0 on macOS 15.4+) and torch>=2.1.
Export to 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")For classification models, pass a list of class names to classifier_names to add a classification head to the CoreML model.
Requires coremltools>=9.0, torch>=1.11, and numpy<=2.3.5. Not supported on Windows.
coremltools>=9.0 ships wheels for Python 3.10–3.13 on macOS and Linux. On newer Python versions the native C extension fails to load. Use Python 3.10–3.13 for CoreML export.
Export to TensorFlow SavedModel#
TF SavedModel export goes through ONNX as an intermediate step:
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")The function returns a Keras model and also generates FP32 and FP16 LiteRT files (.tflite) inside the output directory:
resnet18_saved_model/
├── saved_model.pb
├── variables/
├── assets/
├── fingerprint.pb
├── resnet18_float32.tflite
└── resnet18_float16.tflitePass quantize=8 to add an INT8 .tflite alongside them.
Requirements:
tensorflow>=2.0.0,<=2.19.0onnx2tf>=1.26.3,<1.29.0tf_keras<=2.19.0sng4onnx>=1.0.1onnx_graphsurgeon>=0.3.26ai-edge-litert>=1.2.0,<1.4.0on macOS (ai-edge-litert>=1.2.0on other platforms)onnxslim>=0.1.82onnx>=1.12.0,<2.0.0protobuf>=5
Export to TensorFlow Frozen Graph#
Continuing from the SavedModel export above, convert the returned keras_model to a frozen .pb graph:
from pathlib import Path
from ultralytics.utils.export import keras2pb
keras2pb(keras_model, output_file=Path("resnet18_saved_model/resnet18.pb"))Export to NCNN#
from ultralytics.utils.export import torch2ncnn
torch2ncnn(model, im, output_dir="resnet18_ncnn_model")The directory contains fixed-name param and bin files along with a Python wrapper:
resnet18_ncnn_model/
├── model.ncnn.param
├── model.ncnn.bin
└── model_ncnn.pytorch2ncnn() checks for ncnn and pnnx on first use.
Export to MNN#
MNN export requires an ONNX file as input. Export to ONNX first, then convert:
from ultralytics.utils.export import onnx2mnn, torch2onnx
torch2onnx(model, im, output_file="resnet18.onnx")
onnx2mnn("resnet18.onnx", output_file="resnet18.mnn")Supports quantize=16 for FP16 and quantize=8 for INT8 quantization. Requires MNN>=2.9.6 and torch>=1.10.
Export to PaddlePaddle#
from ultralytics.utils.export import torch2paddle
torch2paddle(model, im, output_dir="resnet18_paddle_model")The directory contains the PaddlePaddle model and parameter files:
resnet18_paddle_model/
├── model.pdmodel
└── model.pdiparamsRequires x2paddle and the correct PaddlePaddle distribution for your platform:
paddlepaddle-gpu>=3.0.0,<3.3.0on CUDApaddlepaddle==3.0.0on ARM64 CPUpaddlepaddle>=3.0.0,<3.3.0on other CPUs
Not supported on NVIDIA Jetson.
Export to ExecuTorch#
from ultralytics.utils.export import torch2executorch
torch2executorch(model, im, output_dir="resnet18_executorch_model")The exported .pte file is saved inside the output directory:
resnet18_executorch_model/
└── model.pteRequires torch>=2.9.0 and a matching ExecuTorch runtime (pip install executorch). For runtime usage, see the ExecuTorch integration.
Export to Core AI#
from ultralytics.utils.export import torch2coreai
torch2coreai(model, im, output_file="resnet18.aimodel")The .aimodel asset is a directory:
resnet18.aimodel/
├── main.mlirb
├── main.hash
└── metadata.jsonExport runs on macOS 26 or later on Apple silicon (pip install coreai-torch), and quantize=16 writes an FP16 asset that takes float16 inputs; the asset runs on iOS 27 and macOS 27. See the Core AI integration, including its note on FP16 assets that abort on load.
Verify Your Exported Model#
After exporting, verify numerical parity with the original PyTorch model before shipping. A quick smoke test with ONNXBackend from ultralytics.nn.backends compares outputs and flags tracing or quantization errors early:
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 exportThe tolerance is per format, not global. On a ResNet-18 the FP32 exports land near 1e-6 for ONNX, TF SavedModel and LiteRT, and at exactly 0 for TorchScript. NCNN is the outlier at roughly 1e-2: its CPU runtime enables FP16 packing and arithmetic by default, so an FP32 export still runs in half precision. A difference far above the format's own baseline points to unsupported ops, a wrong input shape, or a model not in eval mode. FP16 and INT8 exports have looser tolerances. Validate on real data instead of random tensors.
For other runtimes, the input tensor name may differ. OpenVINO, for example, uses the model's forward-argument name (typically x for generic models), while torch2onnx defaults to "images".
Run Your Exported Model#
Exported non-YOLO models load back through the normal YOLO() API. The exports above carry no Ultralytics task or input-size metadata, so pass task explicitly and imgsz matching the example tensor you exported with:
from ultralytics import YOLO
results = YOLO("resnet18.onnx", task="classify")("path/to/image.jpg", imgsz=224)
print(results[0].probs.top1)imgsz matters when the export has a fixed input shape: the ONNX and TF SavedModel exports above reject the default of 640. The TorchScript and NCNN exports above do accept other sizes, but neither exporter guarantees it: both trace from the example tensor, so a model that flattens into a Linear layer stays fixed. Check your own export.
The value is then rounded up to a multiple of the model stride, which is 32 without metadata. A fixed-shape export at 200x200 is therefore fed 224x224 and rejected even though imgsz=200 matches it. For input sizes that are not multiples of 32, call the backend directly.
Calling a Backend Directly#
For raw tensors without Ultralytics preprocessing and post-processing, use the per-format classes in ultralytics.nn.backends, as the verification example above does. Each takes the exported artifact and a device, and is callable:
| Format | Backend | Input layout |
|---|---|---|
| ONNX | ONNXBackend | BCHW |
| TorchScript | TorchScriptBackend | BCHW |
| OpenVINO | OpenVINOBackend | BCHW |
| CoreML | CoreMLBackend | BHWC |
| TF SavedModel, Frozen Graph | TensorFlowBackend | BHWC |
| LiteRT | LiteRTBackend | BCHW |
| NCNN | NCNNBackend | BCHW |
| PaddlePaddle | PaddleBackend | BCHW |
| MNN | MNNBackend | BCHW |
| ExecuTorch | ExecuTorchBackend | BCHW |
| Core AI | CoreAIBackend | BCHW |
TensorFlowBackend covers two formats and defaults to format="saved_model", so pass format="pb" for a frozen graph.
Three things the YOLO() route handles for you and a direct call does not:
- Input layout:
CoreMLBackendandTensorFlowBackendexpect BHWC. Transpose first withim.permute(0, 2, 3, 1); a BCHW tensor raises a shape mismatch. - Autograd: wrap calls in
torch.inference_mode().TorchScriptBackendreturns a tensor that still carries a gradient graph. - Post-processing: without metadata a backend leaves
taskasNoneandnamesempty.LiteRTBackendstill denormalizes any 3-D output by image size on the assumption it holds YOLO boxes, which is wrong for a non-YOLO model with a 3-D output. Two-dimensional outputs such as classifier logits are unaffected.
Known Limitations#
- Multi-input support is uneven:
torch2onnxandtorch2openvinoaccept a tuple or list of example tensors for models with multiple inputs.torch2torchscript,torch2coreml,torch2ncnn,torch2paddle,torch2executorch, andtorch2coreaiassume a single input tensor. - ExecuTorch needs
flatc: The ExecuTorch runtime requires the FlatBuffers compiler. Install withbrew install flatbufferson macOS orapt install flatbuffers-compileron Ubuntu. - No embedded metadata: the exports above carry no Ultralytics task or input-size metadata, so
YOLO()cannot infer either and needs both passed explicitly. See Run Your Exported Model. - YOLO-only formats: Axelera and Sony IMX500 exports require YOLO-specific model attributes and are not available for generic models.
- Platform-specific formats: TensorRT requires an NVIDIA GPU. RKNN requires the
rknn-toolkit2SDK (Linux only). Edge TPU requires theedgetpu_compilerbinary (Linux only).
Conclusion#
These utilities take any PyTorch model from a plain torch.nn.Module to a deployment-ready ONNX, OpenVINO, CoreML, TensorFlow, or mobile-runtime artifact through one consistent API. Pick the format that matches your target hardware, verify numerical parity against the original model, then follow the matching integration guide for runtime-specific deployment steps.
FAQ#
Any
torch.nn.Module. This includes models from timm, torchvision, or any custom PyTorch model. The model must be in evaluation mode (model.eval()) before export. ONNX and OpenVINO additionally accept a tuple of example tensors for multi-input models.All supported formats (TorchScript, ONNX, OpenVINO, CoreML, TF SavedModel, TF Frozen Graph, NCNN, PaddlePaddle, MNN, ExecuTorch, Core AI) can export on CPU. No GPU is required for the export process itself. TensorRT is the only format that requires an NVIDIA GPU.
Use Ultralytics
>=8.4.38, which includes theultralytics.utils.exportmodule and the standardizedoutput_file/output_dirarguments.Yes. torchvision classifiers, detectors, and segmentation models export to
.mlpackageviatorch2coreml. For image classification models, pass a list of class names toclassifier_namesto bake in a classification head. Run the export on macOS or Linux. CoreML is not supported on Windows. See the CoreML integration for iOS deployment details.Yes, for several formats. Pass
quantize=16for FP16 orquantize=8for INT8 when exporting to OpenVINO, CoreML, or MNN; NCNN and Core AI export FP32 by default, takequantize=16for FP16, and have no INT8 path. INT8 in OpenVINO additionally requires acalibration_datasetargument for post-training quantization. See each format's integration page for quantization trade-offs.Run the original PyTorch model and the exported model on the same input, then compare outputs. Load the exported file with the matching backend (for example,
ONNXBackendfor ONNX) and check the maximum absolute difference. Judge the gap against the format's own baseline. For the ResNet-18 example above, FP32 ONNX, TF SavedModel and LiteRT sit near1e-6, TorchScript at0, and NCNN near1e-2because its CPU runtime defaults to FP16. A much larger gap points to unsupported ops, a wrong input shape, or a model not in eval mode. See Verify Your Exported Model for a runnable example.