Ultralytics YOLO27: Join the waitlist.
Get Started
Train your first YOLO model·Ship·Lesson 10/10
Lessonbeginner

Export for Deployment

Export best.pt to ONNX, OpenVINO, TensorRT, CoreML, LiteRT, or QNN, then prove the export matches the PyTorch model before you ship it.

Your best.pt needs Python and PyTorch to run; phones, browsers, Jetsons, and production servers usually don't have them. Export mode turns the model into a self-contained file for those runtimes in one call. The catch: an export that loads isn't automatically correct, and a careless comparison can make a correct export look broken or hide a broken one.

In this lesson you'll export the pill model to ONNX, run it through the same YOLO API, prove it matches PyTorch on boxes and validation mAP under matched settings, and pick the format your target needs: ONNX, OpenVINO, TensorRT, CoreML, LiteRT, or QNN.

Outcome

You can export a trained model to the right format for a deployment target and prove, with a like-for-like parity check and validation, that the export matches best.pt.

Fast Track
If you already know your way around, here's the short version.
  1. yolo export model=runs/detect/pills_v1/weights/best.pt format=onnx — start here.

  2. OpenVINO for Intel CPUs; TensorRT for NVIDIA GPUs and Jetson; CoreML for Apple; LiteRT for Android; QNN for Snapdragon NPUs.

  3. Compare .pt and export on the same image with rect=False on the .pt side.

  4. Validate both at the training imgsz; mAP50-95 should match.

Hands-on

Worked Example: Export to ONNX#

Ultralytics Platform model Export tab listing deployment formats such as ONNX, TensorRT, CoreML, and OpenVINO

yolo export model=runs/detect/pills_v1/weights/best.pt format=onnx
PyTorch: starting from 'runs/detect/pills_v1/weights/best.pt' with input shape (1, 3, 320, 320) BCHW and output shape(s) (1, 5, 2100) (5.2 MB)
ONNX: export success ✅ 0.9s, saved as 'runs/detect/pills_v1/weights/best.onnx' (9.3 MB)
Predict:         yolo predict task=detect model=runs/detect/pills_v1/weights/best.onnx imgsz=320
Validate:        yolo val task=detect model=runs/detect/pills_v1/weights/best.onnx imgsz=320 data=my_dataset/data.yaml

Line by line:

  • input shape (1, 3, 320, 320): one image, 3 color channels, a fixed 320×320 square — the training imgsz, baked into the file.
  • output shape(s) (1, 5, 2100): 4 box values plus 1 class score for 2,100 candidate boxes; NMS runs outside the graph by default.
  • 9.3 MB: the ONNX file stores FP32 weights, so it's larger than the FP16 .pt.
  • Predict: / Validate:: ready-made commands for the exported file, already at the right imgsz.

A few arguments cover most needs; the ONNX integration guide lists the rest.

ArgumentDefaultUse it to
imgszTraining sizeExport at a different fixed input size, e.g. imgsz=640
dynamicFalseAccept variable input sizes instead of one fixed shape
quantizeNone (FP32)16 for FP16 weights (yolo26n.onnx drops from 9.5 MB to 4.8 MB); 8 for INT8 where the format supports it
nmsNonenms=True embeds NMS in the graph, so the runtime returns final boxes

Worked Example: Prove the Export Matches#

The YOLO class loads exported files too, so the predict code from earlier works unchanged. Compare both files on the same validation image:

from ultralytics import YOLO

image = "my_dataset/images/val/Frame_144.jpg"
pt = YOLO("runs/detect/pills_v1/weights/best.pt")(image, rect=False, verbose=False)[0]  # square input, like ONNX
onnx = YOLO("runs/detect/pills_v1/weights/best.onnx")(image, verbose=False)[0]

print(f"pt:   {len(pt.boxes)} pills, mean conf {pt.boxes.conf.mean():.3f}")
print(f"onnx: {len(onnx.boxes)} pills, mean conf {onnx.boxes.conf.mean():.3f}")
print(f"max box difference: {(pt.boxes.xyxy - onnx.boxes.xyxy).abs().max():.4f} px")
pt:   25 pills, mean conf 0.870
onnx: 25 pills, mean conf 0.870
max box difference: 0.0002 px

Same count, same confidence, boxes identical to a fraction of a pixel. Now the stronger check, validation over the whole split:

from ultralytics import YOLO

pt = YOLO("runs/detect/pills_v1/weights/best.pt").val(data="my_dataset/data.yaml", rect=False, verbose=False)
onnx = YOLO("runs/detect/pills_v1/weights/best.onnx").val(data="my_dataset/data.yaml", verbose=False)
print(f".pt   mAP50-95 {pt.box.map:.4f}")
print(f".onnx mAP50-95 {onnx.box.map:.4f}")
.pt   mAP50-95 0.6570
.onnx mAP50-95 0.6570

Identical. (The .pt file's default rectangular validation scored 0.659: a small difference from input padding, not from the export.)

Once it matches, time it with yolo benchmark on the target device so you know its real speed before you ship.

Pick a Format for Your Target#

The Ultralytics Platform lifecycle: annotate with SAM smart annotation, train on cloud GPUs, and deploy across global regions, with integration partner logos

TargetFormatNotes
Portable default: servers and desktops on Linux, Windows, and MaconnxONNX Runtime on CPU or GPU; the quickest export to verify
NVIDIA GPUs and Jetsonengine (TensorRT)Fastest on NVIDIA; export on the device. See Optimize with TensorRT
Intel CPUs and iGPUsopenvinoReal CPU speedups via Intel kernels. See OpenVINO on CPU
iPhone, iPad, and Mac appscoremlNative; uses the Apple Neural Engine
Android and other mobile or embedded deviceslitertINT8 recommended for mobile
Snapdragon NPUsqnnHexagon HTP acceleration; INT8 weights with 16-bit activations by default; choose the HTP architecture with name
Web browseronnx with ONNX Runtime WebSmaller models are realistic: n and s
from ultralytics import YOLO

model = YOLO("runs/detect/pills_v1/weights/best.pt")

model.export(format="onnx")  # cross-platform: ONNX Runtime
model.export(format="openvino")  # Intel CPU / iGPU
model.export(format="engine", quantize=16)  # NVIDIA TensorRT, FP16 (run on the target GPU)
model.export(format="coreml")  # Apple iOS / macOS
model.export(format="litert", quantize=8, data="my_dataset/data.yaml")  # Android / edge, INT8
model.export(format="qnn", name="73", data="my_dataset/data.yaml")  # Qualcomm Snapdragon NPU (HTP v73); calibrate on your own data

Each export writes next to the weights: best.onnx, best_openvino_model/, best.engine, and so on. Start with ONNX even if you'll end up elsewhere: it's the quickest to verify and the input to several other toolchains. The deployment options guide compares every format, and Choose a Deployment Target walks through the decision.

Model quantization trades precision for size and speed. FP16 (quantize=16) halves the file with accuracy almost unchanged. INT8 (quantize=8) is smaller and faster again but needs calibration images — representative frames the export runs through to choose value ranges; Ultralytics recommends at least 300. Always validate an INT8 model before shipping: the loss can be negligible or large depending on the model and calibration set.

Avoid These Mistakes#

Comparing different inputs

Symptom: the .pt model finds 35 pills on Frame_144.jpg and the ONNX file finds 25, and the export looks broken. Cause: PyTorch inference pads each image to the smallest stride-aligned rectangle (192×320 for these 16:9 frames), while a static ONNX model always takes a 320×320 square. Different input, different low-confidence detections. Fix: pass rect=False to the .pt model when comparing, as in parity.py.

A static export ignores your imgsz

Symptom: you pass imgsz=640 to best.onnx and the log still says 320x320. Cause: a static export has one input size, set at export time. Fix: export at the size you'll run (imgsz=640), or export with dynamic=True.

TensorRT engines are device-specific

A .engine built on an A100 will not run on a Jetson Orin, and may not survive a TensorRT upgrade. Export on the target device, with the TensorRT version it will run. ONNX is portable; engines are not.

INT8 calibrates on COCO8 unless you say otherwise

Leave out data on an INT8 export and Ultralytics warns, then calibrates on the default COCO8 sample — eight images that look nothing like your pills. Pass your own: model.export(format="litert", quantize=8, data="my_dataset/data.yaml").

You've Shipped#

You now have a trained best.pt, validation evidence next to a baseline, an exported file for your runtime, and proof that the export matches the original. That's the full loop — data, train, validate, export, deploy — and production teams run it on a schedule. Ultralytics YOLO in Production picks up from your verified export and covers optimized runtimes, tracking pipelines, multi-stream serving, and monitoring.

Try It

Export your pills_v1/weights/best.pt to ONNX, then run parity.py and val_export.py. Rerun parity.py without rect=False and record how the .pt count changes. Deliver four items: the matched detection count, the maximum box difference, the two mAP50-95 values, and one line naming the format your real deployment target needs and why.

Commit
git add -A && git commit -m "feat(deploy): exported best.pt to onnx for production runtime"
Done When
You've finished the lesson when all of these are true.
  • runs/detect/pills_v1/weights/best.onnx exists and predicts through YOLO().

  • With rect=False, detection counts match and the maximum box difference is below 1 px.

  • ONNX and .pt validation mAP50-95 match under matched settings, and you've named your target format with a reason.

Show solution
from ultralytics import YOLO

# Train
model = YOLO("yolo26n.pt")
model.train(data="my_dataset/data.yaml", epochs=10, imgsz=320, name="pills_e2e")

# Validate
metrics = model.val()
print(f"mAP50-95 = {metrics.box.map:.3f}")  # 0.659

# Export
model.export(format="onnx")
parity.py:      pt 25 pills, onnx 25 pills, max box difference 0.0002 px
without rect:   pt 35 pills (192x320 input) vs onnx 25 (320x320): input mismatch, not an export bug
val_export.py:  .pt 0.6570, .onnx 0.6570
Target:         Intel laptop CPU -> openvino, for faster CPU inference than ONNX Runtime
What's next

Course complete — take the final quiz to earn your Train your first YOLO model certificate, then continue with Ultralytics YOLO in Production.