Ultralytics YOLO27: Join the waitlist.
Get Started
Ultralytics YOLO in Production·Scale and Observe·Lesson 9/9
Lessonintermediate

Cost and Latency Tuning

Profile the whole pipeline, pull the levers that actually move latency and cost in order, and stop at a budget.

Teams lose weeks shaving milliseconds off a model while a database write or an RTSP decoder is the real bottleneck. The big levers are few and well measured: YOLO26s takes 2.2 times longer than YOLO26n on CPU for 7.7 more mAP points, and running detection on every other frame halves the work. This lesson teaches you to profile first, pull the biggest lever that fits, and stop at a budget you set in advance.

Outcome

Profile a pipeline end to end, apply one lever with before-and-after latency and mAP, and justify where you stopped.

Fast Track
If you already know your way around, here's the short version.
  1. Profile first: r.speed stages plus your own decode and sink timings.

  2. Levers in order: smaller model → smaller imgsz → faster runtime and FP16 → INT8 → vid_stride.

  3. Measure latency and mAP before and after every change, on the target hardware and the same val set.

  4. Write the budget first (days, mAP loss, target FPS); when it is spent, improve the data instead.

Hands-on

Measure the Whole Pipeline First#

Every Results object splits its own time into stages:

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
totals = {"preprocess": 0.0, "inference": 0.0, "postprocess": 0.0}
for n, r in enumerate(model.predict(video, stream=True, verbose=False), 1):
    for stage in totals:
        totals[stage] += r.speed[stage]
print({stage: round(ms / n, 1) for stage, ms in totals.items()}, "ms per frame")
{'preprocess': 0.7, 'inference': 36.1, 'postprocess': 0.7} ms per frame

Here inference is 95% of the model's time, so model levers will help. Time decode and your own sinks the same way (time.perf_counter() around each), then apply the rule for what you find:

You measureFix first
60 ms decoding RTSP, 5 ms inferenceHardware decode, lower stream resolution, or more decoder threads
30 ms inference, 100 ms Python postprocessingVectorize with NumPy or move it to a worker process
5 ms inference, 200 ms database writesBatch the writes or make them asynchronous
Inference dominatesThe lever leaderboard below

The Lever Leaderboard#

Try from the top and stop when you meet the budget:

LeverTypical speedupTypical accuracy cost
Smaller model (m → s, s → n)1.5–2.5×4.5–7.7 mAP points on COCO; less on narrow tasks
Smaller imgsz (640 → 480)about 1.8×Small and distant objects first
Faster runtime, FP16about 1.5× on GPUUsually none; verify parity
INT8 with calibrationa further 1.2× on GPU; up to 2.9× on CPU1 point (YOLO26n) to 4–6 points (larger models); QAT recovers most
Fewer frames (vid_stride=N)up to N×Fast objects and short events can be missed
Larger batch (servers)Higher throughput, not lower latencyNone

The numbers come from the YOLO26, TensorRT, and OpenVINO benchmark tables and the measurements below; yours will differ, so measure.

Smaller Model#

The YOLO26 benchmark table on COCO:

ModelmAP50-95CPU ONNX (ms)T4 TensorRT10 (ms)
YOLO26n40.938.91.7
YOLO26s48.687.22.5
YOLO26m53.1220.04.7

On CPU each step down is 2.2–2.5× faster. The COCO accuracy gap is the worst case: a 3-class warehouse detector with large objects usually loses far less from m → n than an 80-class general detector, so the smaller model is always worth testing on your own val set.

Smaller Input#

Compute scales roughly with pixel count, so imgsz=480 needs about 56% of the work of 640. Measured on the test laptop's CPU with ONNX Runtime at batch 1 (median of 25 runs on bus.jpg):

Model640480320
YOLO26n21.3 ms12.1 ms7.1 ms
YOLO26s55.0 ms24.2 ms12.9 ms

Decision rule: if your smallest important object still spans about 10 pixels or more at the new size, try it; if objects are small or distant (drones, shelves far from the camera), keep the resolution and shrink the model instead.

Fewer Frames#

If inference bounds your frame rate and the scene moves slowly, run detection on every Nth frame with vid_stride=N and let the tracker bridge the gaps:

import time

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
model.predict(video, vid_stride=30, verbose=False)  # warm up so the first timing is fair

for stride in (1, 2, 3):
    start, ids, frames = time.perf_counter(), set(), 0
    for r in model.track(video, stream=True, vid_stride=stride, tracker="bytetrack.yaml", classes=[0], verbose=False):
        frames += 1
        if r.boxes.is_track:
            ids.update(r.boxes.id.int().tolist())
    print(f"vid_stride={stride}: {frames} frames in {time.perf_counter() - start:.2f} s, {len(ids)} person IDs")
vid_stride=1: 62 frames in 2.66 s, 32 person IDs
vid_stride=2: 31 frames in 1.19 s, 30 person IDs
vid_stride=3: 20 frames in 0.95 s, 28 person IDs

Half the frames cost about half the time, and the tracker kept 30 of 32 identities. At vid_stride=3 it lost 4, which is the cue to stop for this walking-pace scene.

Cut Cloud Cost#

Ultralytics Platform deployment overview: 59 HTTP requests in 24 hours, 0% error rate, 226 ms P95 latency, and a 1 vCPU, 4 GiB endpoint in us-west2 priced at $0.17 per hour

Cost is instance hours divided by useful work. A card like this one has the two numbers you need: what an hour costs ($0.17 for 1 vCPU and 4 GiB here) and how much work it does (59 requests in 24 hours at 226 ms P95, a mostly idle endpoint). Then:

  • Raise utilization. A GPU at 30% utilization wastes 70% of its bill. Batch streams (previous lessons) or serve through Triton Inference Server.
  • Right-size. Benchmark on a smaller GPU, or on a CPU with OpenVINO, before assuming you need a large one.
  • Use spot or preemptible capacity for batch jobs. Reprocessing archived video tolerates interruptions; live streams do not.

Pitfalls#

Optimizing the model when it is not the bottleneck

Symptom: two weeks of tuning saved 10 ms and the system is still slow. Fix: profile the whole pipeline first and fix the largest stage, whatever it is.

Skipping frames after inference

Symptom: if i % 2: continue inside the results loop saves nothing. Fix: the model already ran on that frame; skip at the source with vid_stride.

Batching for latency

Symptom: throughput rose but each frame now arrives later. Fix: larger batches raise throughput per dollar, not per-frame latency; use them for many streams or offline jobs, not a single real-time camera.

The data lever

When the model levers are spent, better data is the next one. A dataset that matches production lets a smaller model reach the accuracy a larger one needed: a cost cut with no accuracy loss.

Try It

Write an optimization budget (days, maximum mAP loss, target FPS on the target hardware). Profile your pipeline with r.speed plus your own decode and sink timings, pick the top lever that fits the bottleneck, apply it, and record latency and mAP50-95 before and after on the same hardware and val set. Deliver the budget, the profile, the before-and-after table, and where you stopped.

Commit
git add -A && git commit -m "perf(production): tuned model + runtime for target latency budget"
Done When
You've finished the lesson when all of these are true.
  • You know which stage dominates your end-to-end frame time, with numbers.

  • You applied one lever and recorded before-and-after latency and mAP on the same hardware and val set.

  • You stopped at the budget you wrote down and can say what you would try next.

Show solution
from ultralytics import YOLO

if __name__ == "__main__":  # validation starts worker processes on macOS and Windows
    data, img = "coco8.yaml", "https://ultralytics.com/images/bus.jpg"  # use your val YAML and a production frame
    for weights, imgsz in (("yolo26s.pt", 640), ("yolo26n.pt", 640), ("yolo26n.pt", 480)):
        model = YOLO(weights)
        runs = [model(img, imgsz=imgsz, verbose=False)[0].speed["inference"] for _ in range(20)][5:]
        m = model.val(data=data, imgsz=imgsz, batch=1, plots=False, verbose=False).box.map
        print(f"{weights} imgsz={imgsz}: {sorted(runs)[len(runs) // 2]:.1f} ms, mAP50-95 {m:.3f}")

Each row drops the first five runs as warm-up and reports the median of the rest. Choose the first row that meets the latency budget with an mAP loss inside your budget. A worked budget: "two days, at most 1 mAP point, 20 FPS on the store box." If yolo26s at 640 misses 50 ms per frame and yolo26n at 640 meets it within 1 point on your val set, stop there; don't spend the second day on 480.

What's next

Course complete. Take the exam to earn your certificate, then continue with Build with Ultralytics Platform, which runs this workflow as a managed end-to-end system.