Jetson Orin Nano 實戰(二):用 TensorRT 讓 YOLOv8 推論快 4.8 倍

上一篇〈實戰(一)〉已經讓 Jetson Orin Nano 用 PyTorch GPU 跑 YOLOv8n,推論約 17 ms、畫面約 30 FPS。這篇要把模型轉成 NVIDIA 的 TensorRT engine,再用一支專門跑 TensorRT 的 server01.py 執行。

實測結果:推論時間從 17.4 ms 降到 3.6 ms,約快 4.8 倍,偵測結果幾乎一樣。

匯出 TensorRT 的過程有幾個地方很容易讓人以為「當掉了」或「裝不起來」。以下照正確順序寫,第一次就能成功。


前置條件

  • 已完成〈實戰(一)〉:相機正常、torch.cuda.is_available() 為 True、server.py 能夠執行。
  • 本文環境:Jetson Orin Nano 8GB(Super)、JetPack 6.2(TensorRT 10.3.0)、Python 3.10、IMX219 相機。

先看重點:三個最容易走錯的地方

  1. 先手動裝好 ONNX 相關套件:PyPI 上沒有 Jetson 版的 onnxruntime-gpu。如果讓 ultralytics 在匯出時自動安裝,整批安裝會失敗,連 onnx 都沒裝上。→ 步驟 1
  2. 匯出時有好幾分鐘沒有任何輸出,這是正常的:不要以為當掉就中斷它;另外要先開全速,並釋放記憶體。→ 步驟 2、3
  3. 確認跑的真的是 TensorRT:server.py 找不到 engine 時,會默默改用 .pt。所以這篇用只載入 engine 的 server01.py。→ 步驟 4

步驟 1:安裝 ONNX 相關套件(匯出前先裝)

pip3 install onnx onnxslim "numpy<2"
pip3 install https://github.com/ultralytics/assets/releases/download/v0.0.0/onnxruntime_gpu-1.23.0-cp310-cp310-linux_aarch64.whl

onnxruntime-gpu 的 Jetson 版 wheel 取自 Ultralytics 官方 Jetson 指南。安裝 onnx 時 numpy 可能被升級,所以一起加上 "numpy<2"。

確認四個套件都正常:

python3 -c "import numpy; print('numpy', numpy.__version__)"
python3 -c "import cv2; print([l.strip() for l in cv2.getBuildInformation().splitlines() if 'GStreamer' in l])"
python3 -c "import torch; print('torch CUDA', torch.cuda.is_available())"
python3 -c "import onnx, onnxruntime as ort; print('onnx', onnx.__version__, '| providers', ort.get_available_providers())"
檢查項目 應該看到
numpy 1 開頭(例如 1.26.4)
cv2 GStreamer: YES
torch CUDA True
onnxruntime providers 裡有 TensorrtExecutionProvider 與 CUDAExecutionProvider

步驟 2:匯出前的準備:開全速、釋放記憶體

cd ~/yolo_project
pkill -f server.py          # 停掉串流程式,釋放相機與記憶體
sudo nvpmodel -q            # 查看目前的功耗模式(Orin Nano Super 的最高模式為 MAXN_SUPER)
sudo jetson_clocks          # 鎖定最高時脈
free -h                     # 確認可用記憶體

Orin Nano 的 8 GB 記憶體由 CPU 與 GPU 共用。匯出時 TensorRT 需要不少記憶體,請先關掉其他程式。


步驟 3:匯出 TensorRT engine

yolo export model=yolov8n.pt format=engine half=True imgsz=640 device=0 workspace=2
參數 意義
format=engine 輸出 TensorRT engine
half=True 使用 FP16(半精度)
imgsz=640 輸入尺寸;之後推論時必須用同樣的尺寸
workspace=2 限制建構時使用的記憶體(GB),避免 8 GB 機器記憶體不足

停住不動時,不要中斷它

輸出 building FP16 engine as yolov8n.engine 之後,畫面會有好幾分鐘完全沒有新訊息。我實測花了 392 秒(約 6.5 分鐘)。這段時間 TensorRT 正在為這台機器挑選最快的運算方式。

想確認它還在跑,可以另開一個終端機:

sudo tegrastats --interval 2000

在輸出中找 GR3D_FREQ 這個欄位(GPU 使用率):

看到的情況 判斷
GR3D_FREQ 時常有數值(例如 20%~40%) 正在建構,繼續等
GR3D_FREQ 方括號裡的時脈只有 @[305] 左右 jetson_clocks 沒有生效,執行 sudo jetson_clocks(匯出中執行也安全)
RAM 接近滿載,SWAP 一直增加 記憶體不足,中止後改用 workspace=1 重做
超過 20 分鐘,GR3D_FREQ 一直是 0% 真的卡住了,按 Ctrl + C 中止後重做

注意:GR3D_FREQ 是輸出裡的欄位名稱,不是指令的一部分。指令只要打 sudo tegrastats --interval 2000;看完按 Ctrl + C 離開,不會影響匯出。

完成畫面

Export complete (392.1s)
Results saved to /home/<使用者>/yolo_project/yolov8n.engine

確認檔案大小:

ls -lh yolov8n.engine       # 約 9 MB

engine 檔和這台機器的 GPU 與 TensorRT 版本綁定。換機器或升級 JetPack 後,要重新匯出。


步驟 4:建立 server01.py(TensorRT 專用版)

server01.py 是以上一篇的 server.py 為基礎修改的,差別在於:

  • 只載入 .engine:找不到 engine,或檔案小於 1 MB(匯出中斷時留下的半成品),就直接停止並印出匯出指令,不會默默改用 .pt。
  • 啟動前先暖機 3 次:TensorRT 前幾次推論會花時間配置記憶體,先跑掉再開始計時,數字比較準。
  • 畫面標示 TensorRT:左下角以橘色字顯示 TensorRT | FPS … | infer … ms,推論時間取移動平均。
  • 可以用 IMGSZ 環境變數指定推論尺寸,必須和匯出時的 imgsz 相同(預設 640)。

一樣建議用 VS Code Remote-SSH 或 scp 放到 ~/yolo_project/,不要直接貼進終端機。

完整程式碼

"""TensorRT 版 YOLO 串流:只載入 .engine,不會自動退回 .pt。"""
import os
import socket
import threading
import time

import cv2
import numpy as np
from flask import Flask, Response
from ultralytics import YOLO

SENSOR_ID = int(os.environ.get("SENSOR_ID", 0))   # 單顆相機用 0
WIDTH, HEIGHT, FPS = 1280, 720, 30
ENGINE_PATH = os.environ.get("MODEL", "yolov8n.engine")
IMGSZ = int(os.environ.get("IMGSZ", 640))         # 必須與匯出 engine 時的 imgsz 相同

if not ENGINE_PATH.endswith(".engine"):
    raise SystemExit(f"❌ server01.py 只跑 TensorRT engine,收到的是:{ENGINE_PATH}\n"
                     "   要跑 .pt 模型請改用 server.py")

# 匯出中斷可能留下不完整的檔案,正常的 yolov8n.engine 約 8–10 MB
if not os.path.exists(ENGINE_PATH) or os.path.getsize(ENGINE_PATH) < 1_000_000:
    raise SystemExit(
        f"❌ 找不到可用的 TensorRT engine:{ENGINE_PATH}\n"
        "   請先停止 server.py,再匯出(約 5–15 分鐘,停在 building FP16 engine 時沒有輸出是正常的):\n"
        f"   yolo export model=yolov8n.pt format=engine half=True imgsz={IMGSZ} device=0 workspace=2")

app = Flask(__name__)
print(f"正在載入 TensorRT engine:{ENGINE_PATH}")
model = YOLO(ENGINE_PATH, task='detect')

# 暖機:TensorRT 前幾次推論會配置記憶體與初始化 context,先跑掉再開始量測
dummy = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)
for _ in range(3):
    model.predict(source=dummy, imgsz=IMGSZ, half=True, verbose=False)
print("✅ TensorRT engine 暖機完成")

gst_pipeline = (
    f"nvarguscamerasrc sensor-id={SENSOR_ID} ! "
    f"video/x-raw(memory:NVMM), width={WIDTH}, height={HEIGHT}, format=NV12, framerate={FPS}/1 ! "
    "nvvidconv flip-method=0 ! video/x-raw, format=BGRx ! "
    "videoconvert ! video/x-raw, format=BGR ! "
    "appsink drop=true max-buffers=1 sync=false"
)

print(f"正在嘗試開啟相機 (sensor-id={SENSOR_ID}),請稍候...")
cap = cv2.VideoCapture(gst_pipeline, cv2.CAP_GSTREAMER)

# GStreamer 失敗時 isOpened() 仍可能是 True,必須實際讀一張才算成功
ok, _ = cap.read() if cap.isOpened() else (False, None)
if not ok:
    cap.release()
    print("❌ 無法從相機取得畫面。請檢查:\n"
          "   1. server.py 是否還在執行(相機同時只能被一個程式使用)\n"
          "   2. ls /dev/video* 是否存在\n"
          "   3. sudo systemctl restart nvargus-daemon")
    raise SystemExit(1)

print(f"✅ 相機成功啟動!模型:{ENGINE_PATH}(TensorRT)")

# 單一背景執行緒負責讀相機 + 推論,所有連線共享最新一張 JPEG
latest_jpeg = None
lock = threading.Lock()


def capture_loop():
    global latest_jpeg
    fps_avg = 0.0
    infer_avg = 0.0
    while True:
        t0 = time.time()
        success, frame = cap.read()
        if not success:
            print("無法讀取相機畫面,0.5 秒後重試")
            time.sleep(0.5)
            continue

        results = model.predict(source=frame, conf=0.4, imgsz=IMGSZ, half=True, verbose=False)
        r = results[0]
        frame = r.plot()

        # 左下角黑底顯示 FPS 與推論時間(移動平均,數字較穩定)
        fps_avg = 0.9 * fps_avg + 0.1 * (1.0 / max(time.time() - t0, 1e-6))
        infer_avg = 0.9 * infer_avg + 0.1 * r.speed['inference']
        label = f"TensorRT | FPS {fps_avg:.1f} | infer {infer_avg:.1f} ms"
        h = frame.shape[0]
        cv2.rectangle(frame, (0, h - 40), (560, h), (0, 0, 0), -1)
        cv2.putText(frame, label, (10, h - 12), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 200, 255), 2)

        ret, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
        if ret:
            with lock:
                latest_jpeg = buffer.tobytes()


def generate_frames():
    while True:
        with lock:
            jpeg = latest_jpeg
        if jpeg is not None:
            yield (b'--frame\r\n'
                   b'Content-Type: image/jpeg\r\n\r\n' + jpeg + b'\r\n')
        time.sleep(1 / FPS)


def lan_ip():
    """取得 Jetson 在區域網路上的 IP(UDP connect 不會真的送出封包)。"""
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 80))
        return s.getsockname()[0]
    except OSError:
        return "<Jetson-IP>"
    finally:
        s.close()


@app.route('/')
def video_feed():
    return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')


if __name__ == '__main__':
    threading.Thread(target=capture_loop, daemon=True).start()
    print(f"🚀 伺服器啟動成功!請開啟瀏覽器存取 http://{lan_ip()}:5000")
    app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)

步驟 5:執行並比較

cd ~/yolo_project
pkill -f server.py          # 相機同時只能被一支程式使用
python3 server01.py

成功時會依序看到:

正在載入 TensorRT engine:yolov8n.engine
✅ TensorRT engine 暖機完成
✅ 相機成功啟動!模型:yolov8n.engine(TensorRT)
🚀 伺服器啟動成功!請開啟瀏覽器存取 http://<Jetson-IP>:5000

實測比較(Orin Nano 8GB Super,1280×720,imgsz 640)

項目 PyTorch GPU(server.py) TensorRT FP16(server01.py)
推論時間 17.4 ms 3.6 ms(約快 4.8 倍)
處理迴圈 FPS 約 30 約 40
偵測結果(同一場景) person 0.88 person 0.82
匯出時間/engine 大小 — 392 秒/9.0 MB

處理迴圈可以超過 30 FPS,是因為 IMX219 的 1280×720 模式最高支援 60 fps。用上 TensorRT 後,推論只占每張畫面處理時間的一小部分,瓶頸變成讀取相機、畫偵測框與 JPEG 編碼。


為什麼 TensorRT 快這麼多?

TensorRT 不是讓模型少算一點,而是事先針對這一台 Jetson 的 GPU,把整個模型重新編譯成最佳化的執行計畫:

  1. 層融合:把「卷積 → BatchNorm → 激活函數」合併成一次 GPU 運算,減少記憶體來回搬運。
  2. 省去逐層下指令的開銷:PyTorch 每執行一層,CPU 都要透過 Python 通知 GPU;YOLOv8n 這種小模型,這段開銷占比很高。TensorRT 的執行計畫是固定的,幾乎沒有這段開銷。
  3. 自動調校:匯出那幾分鐘,TensorRT 會在這台機器上實測每一層的各種實作方式,選出最快的。這也是 engine 只能在同一台機器上使用的原因。
  4. 固定輸入尺寸:記憶體可以事先規劃好、重複使用。

FP16 其實不是主因:上一篇的 server.py 也加了 half=True,PyTorch 那邊本來就用半精度。

實務建議:開發、調整階段用 .pt(server.py),修改比較方便;正式部署時改用 TensorRT(server01.py),追求速度。


如果還是遇到問題

症狀 原因與處理
No matching distribution found for onnxruntime-gpu,接著出現 No module named 'onnx' 沒有先做步驟 1 → 手動安裝後再匯出
numpy.core.multiarray failed to import 安裝 onnx 時 numpy 被升級 → pip3 install "numpy<2"
匯出時出現 NvMapMemAllocInternalTagged: ... error 12 記憶體不足 → 停掉其他程式,改用 workspace=1
server01.py 顯示找不到可用的 engine 匯出沒完成,或檔案不完整 → 重新匯出,確認約 9 MB
無法從相機取得畫面 server.py 還在執行 → pkill -f server.py
Failed to create CaptureSession 前一支程式沒有正常釋放相機 → sudo systemctl restart nvargus-daemon

結語

兩篇做下來,一台 Jetson Orin Nano 就能以 3.6 ms 的推論速度即時偵測物件,GPU 還留有大量餘裕:可以換用更準確的 yolov8s、同時接兩顆相機,或者降低功耗模式來省電。

留言

這個網誌中的熱門文章

在 VS Code 安裝Opencode

使用VS code開發Python程式 - Matplotlib繪圖

LabVIEW教學: 如何在VI中使用.dll檔