A model that scores well in a notebook is not a product. For SUAS we trained a YOLOv11m detector to mAP50 = 0.904 and mAP50-95 = 0.691, and none of that matters if the aircraft cannot run it. The competition does not happen on a desktop GPU. It happens on a Jetson bolted into an airframe, sharing a power budget with everything else on board.
Getting from best.pt to something that flies is its own piece of engineering.
This post is that piece.

My own sketch, and the bars are representative rather than measured. The shape is the point: accuracy stays flat while throughput climbs.
The checkpoint is not the finish line
PyTorch is a training framework. It carries a dynamic graph, autograd machinery and a Python interpreter everywhere it goes. On a workstation you never notice the overhead. On an embedded board with a fixed thermal envelope, you notice it immediately: the same model, the same weights, and a frame rate that makes the whole system pointless.
The weights are maybe a third of the deployment problem. The rest is how those weights get executed.
Everyone who hits this for the first time has the same reaction: blame the board. The board is not guilty. The same silicon can run the same weights far faster; you just cannot carry the comfort of a training framework into the field. Every piece of flexibility that helped you in training is a tax you pay at inference.
What TensorRT actually does
TensorRT takes your network and compiles it for one specific GPU. Three things happen that matter.
It fuses layers: a convolution, its batch norm and its activation become a single kernel instead of three trips through memory. It benchmarks kernel implementations on your actual device and keeps the fastest one for each layer. And it plans memory ahead of time instead of allocating on the fly.
The output is an engine file, and that engine is specific to the GPU and the TensorRT version it was built with. Build it on the Jetson itself, not on your desktop. And when you upgrade JetPack, plan to rebuild: an engine built against one TensorRT version will not load against another. I wrote about the JetPack side of this in the Jetson setup post.
FP16: half the bits, and usually nothing lost
FP16 stores weights and activations in 16 bits instead of 32. Half the memory traffic, and on Jetson-class hardware it unlocks the fast tensor core paths. The question everyone asks is what it costs in accuracy.
The honest answer is: measure it, because the answer is empirical. For our detector, validation results from the FP16 engine matched the FP32 checkpoint within noise. That is the common case for detection models, but it is not a law of nature. Run your full validation set through the actual engine, not the checkpoint, and compare. If the numbers hold, FP16 is free performance.
Why do I refuse to trust it unmeasured? Because FP16 has a narrower representable range than FP32, and in some networks intermediate activations lean on the edge of that range. In detection models this rarely turns into a visible problem, but when it does, it shows up not in the metrics table but in the field, as the occasional target that slips through. A few minutes of validation turns that uncertainty into a non-topic.
INT8 is the next step down, and it is a different animal: it needs a calibration dataset, and accuracy starts requiring real attention. My rule is simple. Take FP16 by default, earn INT8 only if you still need the headroom.
From checkpoint to engine: the chain I actually run
My chain is deliberately boring: PyTorch checkpoint to ONNX, ONNX to TensorRT engine, engine onto the Jetson. ONNX is the handoff point. Exporting freezes the dynamic PyTorch graph into a static one, and it splits the problem cleanly: anything wrong before the ONNX file is a training-side bug, anything wrong after it is a deployment-side bug. When something breaks, that split tells you which half of the system to open.
Two practical notes from running this chain more times than I can count. First, pin your input resolution at export time and make it the same one you train and validate at. A detector exported at one size and fed another will run without a single error message and score garbage. Second, treat the ONNX file as a real artifact, not a temp file. Version it next to the checkpoint it came from, because when a field result looks off two weeks later, “which export was this engine built from” is the first question you will ask.
There is also the version question. All three links of the chain, PyTorch, the ONNX export and TensorRT, carry their own compatibility story. When you find a combination that works, write it down. “Latest version” is not a virtue on the embedded side; it is a risk.
Ultralytics can collapse this chain into one command and go straight from checkpoint to engine. I still like the explicit ONNX step for one reason: it gives you a place to stop and inspect before the engine bakes everything in.
Verify on the device, not in your head
The export itself is short: Ultralytics can emit a TensorRT engine directly, or you
export ONNX and build with trtexec. The part people skip is verification, so here
is my list.
Run the validation set through the deployed engine and compare metrics against the
checkpoint. Confirm the preprocessing matches: letterboxing, normalisation, channel
order. A silent mismatch here costs you accuracy that no profiler will ever show
you. Confirm the postprocessing matches too, because NMS and confidence
thresholds are part of the model whether you
like it or not. And watch tegrastats while it runs, because a thermally
throttling Jetson will quietly take back everything the optimisation gave you.
I do not keep this list in my head; I keep it as a script. When an export finishes, one command runs the validation set through the engine, writes the metrics next to the checkpoint’s, and stamps the preprocessing parameters at the top of the output. A check left to human memory is the first check skipped in delivery week.
On the defence side we run the same discipline on visible and thermal channels from C++, and nothing about the list changes. The engine is a contract between training and the field. Test the contract, not the intention.
Case study: the SUAS detector, from best.pt to the airframe
Everything above sounds abstract until you attach it to one aircraft, so here is ours. The SUAS detector is a YOLOv11m trained on roughly 15,000 images. The objects it has to find, mannequins and tents laid out on the ground, are 80 to 120 pixels across in a 4K frame at 20 to 40 m altitude. Resize that frame down to a normal detector input and the targets dissolve, so we tile each frame into 1280x1280 crops and run detection per tile. Training converged around epoch 33 and early stopping closed the run at epoch 48, ending at mAP50 = 0.904 and mAP50-95 = 0.691 on the validation set.
The deployment chain was exactly the one described above: PyTorch checkpoint to ONNX, ONNX to a TensorRT FP16 engine, engine built on the Jetson that actually flies. FP16 mattered here for a plain reason: tiling multiplies the work. One 4K frame becomes a stack of 1280x1280 tiles, and the Jetson has to keep up with that inside the airframe’s power and thermal budget. The FP16 engine is what made the tiling budget close.
The habit I want to sell you on is the re-measuring. After every export, not just the first one, we ran the full validation set through the engine itself and put the numbers next to the checkpoint’s. Same data, same metrics, different executor. Most days that comparison is boring, which is the point: on the one day it is not boring, you have caught a preprocessing mismatch or a broken export before it flew instead of after. I wrote about why this gap exists in the gap between the notebook and the board. The short version: a model changes executors on its way to the field, and every change is a chance to lose accuracy silently.
The same caution applied one level up. The detector feeds a mapping pipeline that stitches tiles back into a full view, and we validated that stitching over a mock city on the ground before trusting it over a real field. Cheap rehearsal, expensive lesson avoided.

Frame: real output from the project.