Comment exporter des modèles PyTorch non-YOLO avec Ultralytics#
Ultralytics propose des utilitaires d'exportation autonomes sous ultralytics.utils.export qui englobent de multiples backends derrière une interface cohérente. Tu peux exporter n'importe quel torch.nn.Module, y compris des modèles d'image timm, des classificateurs et détecteurs torchvision, ou tes propres architectures personnalisées, vers ONNX, TorchScript, OpenVINO, CoreML, NCNN, PaddlePaddle, MNN, ExecuTorch, Core AI, TensorFlow SavedModel et TensorFlow Frozen Graph sans avoir à apprendre chaque backend séparément.
Déployer des modèles PyTorch en production implique généralement de jongler avec un exportateur différent pour chaque cible : torch.onnx.export pour ONNX, coremltools pour les appareils Apple, onnx2tf pour TensorFlow, pnnx pour NCNN, et ainsi de suite. Chaque outil possède sa propre API, ses particularités en matière de dépendances et ses conventions de sortie. Ces utilitaires regroupent tout cela en un seul modèle d’appel.
Pourquoi utiliser Ultralytics pour l’export de modèles non-YOLO ?#
- Une API unique sur 11 formats : apprends une seule convention d'appel au lieu d'une douzaine.
- Une interface d’utilitaires commune : les assistants d’export se trouvent sous
ultralytics.utils.export; une fois les packages des backends installés, tu peux conserver le même modèle d’appel entre les formats. - Le même chemin de code que pour les exports YOLO : les mêmes assistants alimentent tous les exports YOLO d’Ultralytics.
- Quantification FP16 et INT8 intégrée pour les formats qui la prennent en charge (OpenVINO, CoreML et MNN ; FP16 uniquement pour NCNN et Core AI).
- Fonctionne sur CPU : aucun GPU n'est requis pour l'étape d'exportation elle-même, tu peux donc l'exécuter localement sur un ordinateur portable ; l'exportation CoreML n'est pas prise en charge sur Windows, et l'exportation Core AI nécessite macOS 26 ou une version ultérieure sur les puces Apple silicon.
Démarrage rapide#
Le chemin le plus rapide consiste à effectuer un export en deux lignes vers ONNX, sans code YOLO et sans autre configuration 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")Formats d’export pris en charge#
Les fonctions torch2* prennent un torch.nn.Module standard et un tenseur d’entrée d’exemple. MNN, TF SavedModel et TF Frozen Graph passent par un artefact intermédiaire ONNX ou Keras. Dans les deux cas, aucun attribut spécifique à YOLO n’est requis.
| Format | Fonction | Installation | Sortie |
|---|---|---|---|
| ONNX | torch2onnx() | pip install onnx | fichier .onnx |
| TorchScript | torch2torchscript() | inclus avec PyTorch | fichier .torchscript |
| OpenVINO | torch2openvino() | pip install openvino | répertoire _openvino_model/ |
| CoreML | torch2coreml() | pip install coremltools | .mlpackage |
| TF SavedModel | onnx2saved_model() | voir les exigences détaillées ci-dessous | répertoire _saved_model/ |
| TF Frozen Graph | keras2pb() | voir les exigences détaillées ci-dessous | fichier .pb |
| NCNN | torch2ncnn() | pip install ncnn pnnx | répertoire _ncnn_model/ |
| MNN | onnx2mnn() | pip install MNN | fichier .mnn |
| PaddlePaddle | torch2paddle() | pip install paddlepaddle x2paddle | répertoire _paddle_model/ |
| ExecuTorch | torch2executorch() | pip install executorch | répertoire _executorch_model/ |
| Core AI | torch2coreai() | pip install coreai-torch (macOS 26+ sur Apple silicon) | répertoire .aimodel |
Les exports MNN, TF SavedModel et TF Frozen Graph passent par ONNX comme étape intermédiaire. Exporte d’abord vers ONNX, puis convertis.
Plusieurs fonctions d’export acceptent un dictionnaire metadata facultatif (par exemple, torch2torchscript(..., metadata={"author": "me"})) qui intègre des paires clé-valeur personnalisées dans l’artefact exporté lorsque le format le permet.
Exemples étape par étape#
Chaque exemple ci-dessous utilise la même configuration : un ResNet-18 préentraîné provenant de timm en mode évaluation :
import timm
import torch
model = timm.create_model("resnet18", pretrained=True).eval()
im = torch.randn(1, 3, 224, 224)Le dropout, la normalisation par lots et les autres couches utilisées uniquement pendant l’entraînement se comportent différemment lors de l’inférence. Omettre .eval() produit des exports avec des sorties incorrectes.
Exporter vers ONNX#
from ultralytics.utils.export import torch2onnx
torch2onnx(model, im, output_file="resnet18.onnx")Pour une taille de lot dynamique, transmets un dictionnaire dynamic :
torch2onnx(model, im, output_file="resnet18_dyn.onnx", dynamic={"images": {0: "batch_size"}})L’opset par défaut est 14 et le nom d’entrée par défaut est "images". Remplace-les avec les arguments opset, input_names ou output_names.
Exporter vers TorchScript#
Aucune dépendance supplémentaire n’est nécessaire. Utilise torch.jit.trace en interne.
from ultralytics.utils.export import torch2torchscript
torch2torchscript(model, im, output_file="resnet18.torchscript")Exporter vers OpenVINO#
from ultralytics.utils.export import torch2openvino
ov_model = torch2openvino(model, im, output_dir="resnet18_openvino_model")Le répertoire contient une paire model.xml et model.bin aux noms fixes :
resnet18_openvino_model/
├── model.xml
└── model.binTransmets dynamic=True pour des formes d’entrée dynamiques, quantize=16 pour FP16 ou quantize=8 pour la quantification INT8. INT8 nécessite également un argument calibration_dataset.
Nécessite openvino>=2024.0.0 (ou >=2025.2.0 sur macOS 15.4 ou version ultérieure) et torch>=2.1.
Exporter vers 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")Pour les modèles de classification, transmets une liste de noms de classes à classifier_names afin d’ajouter une tête de classification au modèle CoreML.
Nécessite coremltools>=9.0, torch>=1.11 et numpy<=2.3.5. Non pris en charge sous Windows.
coremltools>=9.0 fournit des wheels pour Python 3.10 à 3.13 sur macOS et Linux. Avec les versions plus récentes de Python, l’extension C native ne se charge pas. Utilise Python 3.10 à 3.13 pour l’export CoreML.
Exporter vers TensorFlow SavedModel#
L’export TF SavedModel passe par ONNX comme étape intermédiaire :
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 fonction renvoie un modèle Keras et génère également des fichiers LiteRT FP32 et FP16 (.tflite) dans le répertoire de sortie :
resnet18_saved_model/
├── saved_model.pb
├── variables/
├── assets/
├── fingerprint.pb
├── resnet18_float32.tflite
└── resnet18_float16.tfliteTransmets quantize=8 pour ajouter un .tflite INT8 à leurs côtés.
Exigences :
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.0sur macOS (ai-edge-litert>=1.2.0sur les autres plateformes)onnxslim>=0.1.82onnx>=1.12.0,<2.0.0protobuf>=5
Exporter vers TensorFlow Frozen Graph#
En poursuivant à partir de l’export SavedModel ci-dessus, convertis le keras_model renvoyé en graphe .pb gelé :
from pathlib import Path
from ultralytics.utils.export import keras2pb
keras2pb(keras_model, output_file=Path("resnet18_saved_model/resnet18.pb"))Exporter vers NCNN#
from ultralytics.utils.export import torch2ncnn
torch2ncnn(model, im, output_dir="resnet18_ncnn_model")Le répertoire contient des fichiers param et bin aux noms fixes, ainsi qu’un wrapper Python :
resnet18_ncnn_model/
├── model.ncnn.param
├── model.ncnn.bin
└── model_ncnn.pytorch2ncnn() vérifie la présence de ncnn et pnnx lors de la première utilisation.
Exporter vers MNN#
L’export MNN nécessite un fichier ONNX en entrée. Exporte d’abord vers ONNX, puis convertis :
from ultralytics.utils.export import onnx2mnn, torch2onnx
torch2onnx(model, im, output_file="resnet18.onnx")
onnx2mnn("resnet18.onnx", output_file="resnet18.mnn")Prend en charge quantize=16 pour FP16 et quantize=8 pour la quantification INT8. Nécessite MNN>=2.9.6 et torch>=1.10.
Exporter vers PaddlePaddle#
from ultralytics.utils.export import torch2paddle
torch2paddle(model, im, output_dir="resnet18_paddle_model")Le répertoire contient le modèle PaddlePaddle et les fichiers de paramètres :
resnet18_paddle_model/
├── model.pdmodel
└── model.pdiparamsNécessite x2paddle et la distribution PaddlePaddle appropriée pour ta plateforme :
paddlepaddle-gpu>=3.0.0,<3.3.0sur CUDApaddlepaddle==3.0.0sur CPU ARM64paddlepaddle>=3.0.0,<3.3.0sur les autres CPU
Non pris en charge sur NVIDIA Jetson.
Exporter vers ExecuTorch#
from ultralytics.utils.export import torch2executorch
torch2executorch(model, im, output_dir="resnet18_executorch_model")Le fichier .pte exporté est enregistré dans le répertoire de sortie :
resnet18_executorch_model/
└── model.pteNécessite torch>=2.9.0 et un runtime ExecuTorch correspondant (pip install executorch). Pour l’utilisation du runtime, consulte l’intégration ExecuTorch.
Exporter vers Core AI#
from ultralytics.utils.export import torch2coreai
torch2coreai(model, im, output_file="resnet18.aimodel")L'actif .aimodel est un répertoire :
resnet18.aimodel/
├── main.mlirb
├── main.hash
└── metadata.jsonL'exportation s'exécute sur macOS 26 ou une version ultérieure sur Apple silicon (pip install coreai-torch), et quantize=16 écrit un actif FP16 qui prend des entrées float16 ; l'actif s'exécute sur iOS 27 et macOS 27. Consulte l'intégration Core AI, y compris sa note sur les actifs FP16 qui s'arrêtent au chargement.
Vérifier ton modèle exporté#
Après l’export, vérifie la parité numérique avec le modèle PyTorch d’origine avant la mise en production. Un test rapide avec ONNXBackend provenant de ultralytics.nn.backends compare les sorties et signale rapidement les erreurs de traçage ou de quantification :
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 exportLa tolérance dépend du format et n’est pas globale. Sur un ResNet-18, les exports FP32 se situent autour de 1e-6 pour ONNX, TF SavedModel et LiteRT, et exactement à 0 pour TorchScript. NCNN est l’exception, à environ 1e-2 : son runtime CPU active par défaut le conditionnement et les calculs FP16, si bien qu’un export FP32 s’exécute tout de même en demi-précision. Une différence largement supérieure à la référence du format indique des opérateurs non pris en charge, une forme d’entrée incorrecte ou un modèle qui n’est pas en mode évaluation. Les exports FP16 et INT8 ont des tolérances plus larges. Valide-les sur des données réelles plutôt qu’avec des tenseurs aléatoires.
Pour les autres runtimes, le nom du tenseur d’entrée peut différer. OpenVINO, par exemple, utilise le nom de l’argument forward du modèle (généralement x pour les modèles génériques), tandis que torch2onnx prend par défaut la valeur "images".
Exécuter ton modèle exporté#
Les modèles non-YOLO exportés se rechargent via l’API YOLO() habituelle. Les exports ci-dessus ne contiennent aucune métadonnée Ultralytics concernant la tâche ou la taille d’entrée ; transmets donc explicitement task ainsi que imgsz, en faisant correspondre ce dernier au tenseur d’exemple utilisé pour l’export :
from ultralytics import YOLO
results = YOLO("resnet18.onnx", task="classify")("path/to/image.jpg", imgsz=224)
print(results[0].probs.top1)imgsz est important lorsque l’export possède une forme d’entrée fixe : les exports ONNX et TF SavedModel ci-dessus refusent la valeur par défaut 640. Les exports TorchScript et NCNN ci-dessus acceptent effectivement d’autres tailles, mais aucun des deux exportateurs ne le garantit : tous deux effectuent le traçage à partir du tenseur d’exemple, donc un modèle qui aplati les données dans une couche Linear conserve une taille fixe. Vérifie ton propre export.
La valeur est ensuite arrondie à l’entier supérieur pour être un multiple du stride du modèle, qui vaut 32 sans métadonnées. Un export à forme fixe de 200x200 reçoit donc une entrée de 224x224 et est rejeté, même si imgsz=200 lui correspond. Pour les tailles d’entrée qui ne sont pas des multiples de 32, appelle directement le backend.
Appeler directement un backend#
Pour les tenseurs bruts sans prétraitement ni post-traitement Ultralytics, utilise les classes propres à chaque format dans ultralytics.nn.backends, comme le fait l’exemple de vérification ci-dessus. Chacune prend l’artefact exporté et un appareil, et peut être appelée :
| Format | Backend | Disposition de l’entrée |
|---|---|---|
| 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 couvre deux formats et utilise par défaut format="saved_model" ; transmets donc format="pb" pour un graphe gelé.
Voici trois éléments que la route YOLO() gère pour toi, contrairement à un appel direct :
- Disposition de l’entrée :
CoreMLBackendetTensorFlowBackendattendent le format BHWC. Effectue d’abord une transposition avecim.permute(0, 2, 3, 1); un tenseur BCHW provoque une incompatibilité de forme. - Autograd : encapsule les appels dans
torch.inference_mode().TorchScriptBackendrenvoie un tenseur qui contient encore un graphe de gradient. - Post-traitement : sans métadonnées, un backend laisse
tasksous forme deNoneetnamesvide.LiteRTBackenddénormalise tout de même chaque sortie 3D selon la taille de l’image, en supposant qu’elle contient des boîtes YOLO, ce qui est incorrect pour un modèle non-YOLO avec une sortie 3D. Les sorties bidimensionnelles, telles que les logits d’un classifieur, ne sont pas affectées.
Limitations connues#
- La prise en charge multi-entrée est inégale :
torch2onnxettorch2openvinoacceptent un tuple ou une liste de tenseurs d'exemple pour les modèles à entrées multiples.torch2torchscript,torch2coreml,torch2ncnn,torch2paddle,torch2executorchettorch2coreaisupposent un seul tenseur d'entrée. - ExecuTorch nécessite
flatc: le runtime ExecuTorch nécessite le compilateur FlatBuffers. Installe-le avecbrew install flatbufferssur macOS ouapt install flatbuffers-compilersur Ubuntu. - Aucune métadonnée intégrée : les exports ci-dessus ne contiennent aucune métadonnée Ultralytics concernant la tâche ou la taille d’entrée ;
YOLO()ne peut donc déduire ni l’une ni l’autre et nécessite que tu transmettes explicitement les deux. Consulte Exécuter ton modèle exporté. - Formats réservés à YOLO : les exportations Axelera et Sony IMX500 nécessitent des attributs de modèle spécifiques à YOLO et ne sont pas disponibles pour les modèles génériques.
- Formats spécifiques à une plateforme : TensorRT nécessite un GPU NVIDIA. RKNN nécessite le SDK
rknn-toolkit2(Linux uniquement). Edge TPU nécessite le binaireedgetpu_compiler(Linux uniquement).
Conclusion#
Ces utilitaires prennent n'importe quel modèle PyTorch, d'un simple torch.nn.Module à un artefact ONNX, OpenVINO, CoreML, TensorFlow ou d'un runtime mobile prêt pour le déploiement, via une API cohérente. Choisis le format correspondant à ton matériel cible, vérifie la parité numérique avec le modèle d'origine, puis suis le guide d'intégration correspondant pour les étapes de déploiement propres au runtime.
FAQ#
N'importe quel
torch.nn.Module. Cela inclut les modèles de timm, torchvision ou tout modèle PyTorch personnalisé. Le modèle doit être en mode évaluation (model.eval()) avant l'exportation. ONNX et OpenVINO acceptent également un tuple de tenseurs d'exemple pour les modèles à entrées multiples.Tous les formats pris en charge (TorchScript, ONNX, OpenVINO, CoreML, TF SavedModel, TF Frozen Graph, NCNN, PaddlePaddle, MNN, ExecuTorch, Core AI) peuvent être exportés sur CPU. Aucun GPU n'est requis pour le processus d'exportation lui-même. TensorRT est le seul format qui nécessite un GPU NVIDIA.
Utilise Ultralytics
>=8.4.38, qui inclut le moduleultralytics.utils.exportainsi que les arguments standardisésoutput_file/output_dir.Oui. Les modèles de classification, de détection et de segmentation torchvision s'exportent vers
.mlpackageviatorch2coreml. Pour les modèles de classification d'images, transmets une liste de noms de classes àclassifier_namesafin d'intégrer une tête de classification. Effectue l'exportation sur macOS ou Linux. CoreML n'est pas pris en charge sur Windows. Consulte l'intégration CoreML pour les détails du déploiement sur iOS.Oui, pour plusieurs formats. Passe
quantize=16pour FP16 ouquantize=8pour INT8 lors de l'exportation vers OpenVINO, CoreML ou MNN ; l'exportation NCNN et Core AI exporte en FP32 par défaut, prendquantize=16pour FP16 et ne possède pas de chemin INT8. La quantification INT8 dans OpenVINO nécessite en outre un argumentcalibration_datasetpour la quantification post-entraînement. Consulte la page d'intégration de chaque format pour connaître les compromis liés à la quantification.
Exécute le modèle PyTorch d'origine et le modèle exporté sur la même entrée, puis compare les sorties. Charge le fichier exporté avec le backend correspondant (par exemple,
ONNXBackendpour ONNX) et vérifie la différence absolue maximale. Évalue l'écart par rapport à la référence propre au format. Pour l'exemple ResNet-18 ci-dessus, FP32 ONNX, TF SavedModel et LiteRT se situent près de1e-6, TorchScript à0et NCNN près de1e-2, car son runtime CPU utilise par défaut FP16. Un écart nettement plus important indique des ops non prises en charge, une forme d'entrée incorrecte ou un modèle qui n'est pas en mode évaluation. Consulte Vérifier ton modèle exporté pour voir un exemple exécutable.