00 ONNX nedir? — format ve ekosistem
ONNX (Open Neural Network Exchange), farklı derin öğrenme çerçeveleri arasında model taşınabilirliğini sağlayan açık bir format standardıdır. Microsoft, Facebook ve AWS öncülüğünde geliştirilmiştir.
Ekosistem haritası
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ PyTorch │ │TensorFlow│ │ scikit │ │ MXNet │
│ .pt/.pth │ │ saved_ │ │ -learn │ │ .params │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
▼ ▼ ▼ ▼
torch.onnx tf2onnx sklearn-onnx mxnet
.export() convert convert export
│ │ │ │
└──────────────┴──────────────┴──────────────┘
↓
model.onnx (FlatBuffer/ProtoBuf)
↓
ONNX Runtime (libonnxruntime)
↓
┌────────┬───────────┬──────────┬───────┐
│ CPU EP │ CUDA EP │ TensorRT │ NNAPI │
│ x86_64 │ NVIDIA │ EP │Android│
│ ARM │ GPU │ │ │
└────────┴───────────┴──────────┴───────┘
ONNX, TFLite'a kıyasla daha zengin op kümesine sahiptir (transformer, LSTM, custom ops). Ancak MCU deployment'ta TFLite Micro daha uygun seçenektir. ONNX/ORT, Linux tabanlı gömülü sistemlerde (Raspberry Pi, i.MX8, RK3588) tercih edilir.
01 ONNX Runtime mimarisi
ORT'un execution provider (EP) mimarisi, aynı ONNX modelini farklı donanım hızlandırıcılarında çalıştırmayı sağlar. Graph optimizer, session oluşturulurken modeli analiz edip dönüştürür.
Dahili pipeline
model.onnx
↓
[Model Loader]
ProtoBuf parse → InternalGraph
↓
[Graph Optimizer]
├── Level 1 (Basic) : constant folding, redundant elimination
├── Level 2 (Extended) : node fusion (Conv+BN+Relu)
└── Level 3 (All) : layout transformation, memory planning
↓
[Execution Provider Dispatch]
├── CPU EP : libomp, MLAS kernel (ARM NEON otomatik)
├── CUDA EP : cuDNN, cuBLAS kernel dispatch
└── fallback : CPU EP (desteklenmeyen op'lar için)
↓
[Session Run]
input tensors → kernel execution → output tensors
02 Python API — InferenceSession, run, options
ORT Python API'si, prototipleme ve Linux gömülü sistemler için birincil arayüzdür. onnxruntime paketi pip ile kurulur.
Kurulum
# CPU-only (arm64 RPi dahil)
pip install onnxruntime
# GPU destekli (CUDA 12.x)
pip install onnxruntime-gpu
# Gömülü / minimal (ORT Lite)
pip install onnxruntime-extensions # custom op desteği
Temel çıkarım
import onnxruntime as ort
import numpy as np
# Session oluştur — Session options ile optimize et
so = ort.SessionOptions()
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
so.intra_op_num_threads = 4 # işlem içi paralellik
so.inter_op_num_threads = 1 # işlemler arası paralellik
so.enable_mem_pattern = True # bellek yeniden kullanımı
so.enable_cpu_mem_arena = True
session = ort.InferenceSession(
"yolov8n.onnx",
sess_options=so,
providers=["CPUExecutionProvider"]
)
# Model giriş/çıkış bilgisi
for inp in session.get_inputs():
print(f"Input : {inp.name} shape={inp.shape} type={inp.type}")
for out in session.get_outputs():
print(f"Output: {out.name} shape={out.shape} type={out.type}")
# Çıkarım
img = np.random.rand(1, 3, 640, 640).astype(np.float32) # NCHW
outputs = session.run(
output_names=None, # tümünü döndür
input_feed={"images": img}
)
print("Çıkış şekli:", outputs[0].shape) # (1, 84, 8400) YOLOv8 formatı
Gelişmiş: IO Binding ile sıfır kopyalama
import onnxruntime as ort
import numpy as np
session = ort.InferenceSession("yolov8n.onnx",
providers=["CPUExecutionProvider"])
# IOBinding: giriş/çıkış tensörlerini önceden bağla
io_binding = session.io_binding()
img = np.random.rand(1, 3, 640, 640).astype(np.float32)
# Girişi ORT OrtValue olarak bağla (kopya yok)
io_binding.bind_input(
name='images',
device_type='cpu',
device_id=0,
element_type=np.float32,
shape=img.shape,
buffer_ptr=img.ctypes.data
)
# Çıkışı CPU'da oluştur
io_binding.bind_output('output0', 'cpu')
session.run_with_iobinding(io_binding)
output = io_binding.copy_outputs_to_cpu()[0]
print("output shape:", output.shape)
03 C API — OrtApi, OrtSession, OrtValue
ORT C API, C++ ortamı olmayan veya ABI uyumluluğu gerektiren gömülü sistemler için kullanılır. Tüm ORT işlemleri tek bir OrtApi struct üzerinden gerçekleşir.
Temel C çıkarım örneği
#include <onnxruntime_c_api.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ORT_ABORT_ON_ERROR(expr) \
do { OrtStatus* ort_status = (expr); \
if (ort_status != NULL) { \
fprintf(stderr, "ORT Error: %s\n", \
g_ort->GetErrorMessage(ort_status)); \
g_ort->ReleaseStatus(ort_status); \
abort(); \
} \
} while (0)
static const OrtApi* g_ort = NULL;
int main(void) {
g_ort = OrtGetApiBase()->GetApi(ORT_API_VERSION);
/* Environment oluştur */
OrtEnv* env;
ORT_ABORT_ON_ERROR(g_ort->CreateEnv(
ORT_LOGGING_LEVEL_WARNING, "ort_embedded", &env));
/* Session options */
OrtSessionOptions* session_options;
ORT_ABORT_ON_ERROR(g_ort->CreateSessionOptions(&session_options));
ORT_ABORT_ON_ERROR(g_ort->SetIntraOpNumThreads(session_options, 4));
ORT_ABORT_ON_ERROR(g_ort->SetSessionGraphOptimizationLevel(
session_options, ORT_ENABLE_ALL));
/* Session oluştur */
OrtSession* session;
ORT_ABORT_ON_ERROR(g_ort->CreateSession(
env, "yolov8n.onnx", session_options, &session));
/* Memory info */
OrtMemoryInfo* memory_info;
ORT_ABORT_ON_ERROR(g_ort->CreateCpuMemoryInfo(
OrtArenaAllocator, OrtMemTypeDefault, &memory_info));
/* Giriş tensörü oluştur */
float input_data[1 * 3 * 640 * 640];
memset(input_data, 0, sizeof(input_data));
int64_t input_shape[] = {1, 3, 640, 640};
size_t input_shape_len = 4;
OrtValue* input_tensor;
ORT_ABORT_ON_ERROR(g_ort->CreateTensorWithDataAsOrtValue(
memory_info,
input_data, sizeof(input_data),
input_shape, input_shape_len,
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT,
&input_tensor));
/* Çıkarım */
const char* input_names[] = {"images"};
const char* output_names[] = {"output0"};
OrtValue* output_tensor = NULL;
ORT_ABORT_ON_ERROR(g_ort->Run(
session, NULL,
input_names, (const OrtValue* const*)&input_tensor, 1,
output_names, 1, &output_tensor));
/* Çıkış verisine eriş */
float* output_data;
ORT_ABORT_ON_ERROR(g_ort->GetTensorMutableData(
output_tensor, (void**)&output_data));
printf("İlk çıkış değeri: %f\n", output_data[0]);
/* Temizlik */
g_ort->ReleaseValue(output_tensor);
g_ort->ReleaseValue(input_tensor);
g_ort->ReleaseMemoryInfo(memory_info);
g_ort->ReleaseSession(session);
g_ort->ReleaseSessionOptions(session_options);
g_ort->ReleaseEnv(env);
return 0;
}
aarch64-linux-gnu-gcc -O2 -o ort_infer ort_c_infer.c \
-I/opt/ort/include \
-L/opt/ort/lib -lonnxruntime \
-Wl,-rpath,/opt/ort/lib
04 Execution providers — CPU, CUDA, OpenVINO, NNAPI
Her Execution Provider (EP), belirli donanım üzerinde op'ları çalıştırır. ORT, desteklenmeyen op'ları otomatik olarak CPU EP'ye devreder (fallback).
providers=["CUDAExecutionProvider", "CPUExecutionProvider"] ile aktif edilir. CUDA EP desteklemediği op'ları CPU'ya devreder.providers=["NnapiExecutionProvider"].EP seçimi ve fallback akışı
import onnxruntime as ort
# Mevcut EP'leri listele
print(ort.get_available_providers())
# ['CPUExecutionProvider'] — RPi4 üzerinde tipik çıktı
# ['CUDAExecutionProvider','CPUExecutionProvider'] — CUDA kurulu
# RPi4 için optimal konfigürasyon
providers = [
("CPUExecutionProvider", {
"arena_extend_strategy": "kNextPowerOfTwo",
"cpu_memory_arena_cfg": "max_mem:536870912", # 512 MB limit
})
]
session = ort.InferenceSession("yolov8n.onnx", providers=providers)
# Hangi EP'nin aktif olduğunu doğrula
for i, node_provider in enumerate(session._provider_options):
print(f"Node group {i}: {node_provider}")
05 Model optimizasyonu — graph opt, QDQ quantization
ORT'un üç seviyeli graph optimizer ve ONNX Runtime quantization araçları, çıkarım süresini ve bellek kullanımını önemli ölçüde azaltır.
Graph optimization seviyeleri
| Seviye | Enum | İçerik | Etki |
|---|---|---|---|
| Devre dışı | ORT_DISABLE_ALL | Ham grafik | — |
| Basic | ORT_ENABLE_BASIC | Constant folding, redundant elim. | %5-10 |
| Extended | ORT_ENABLE_EXTENDED | Op fusion (Conv+BN, Gelu fused) | %15-25 |
| All | ORT_ENABLE_ALL | Layout dönüşümü, memory planner | %20-40 |
QDQ (Quantize-DeQuantize) INT8 quantization
from onnxruntime.quantization import quantize_static, CalibrationDataReader
from onnxruntime.quantization import QuantFormat, QuantType
import numpy as np
class ImageCalibrationReader(CalibrationDataReader):
def __init__(self, model_path, image_folder, num_samples=100):
import glob, cv2
self.images = glob.glob(f"{image_folder}/*.jpg")[:num_samples]
self.index = 0
def get_next(self):
if self.index >= len(self.images):
return None
import cv2
img = cv2.imread(self.images[self.index])
img = cv2.resize(img, (640, 640))
img = img.astype(np.float32) / 255.0
img = img.transpose(2, 0, 1)[np.newaxis] # NCHW
self.index += 1
return {"images": img}
# Statik INT8 quantization (QDQ format, CPU EP için optimal)
quantize_static(
model_input="yolov8n.onnx",
model_output="yolov8n_int8.onnx",
calibration_data_reader=ImageCalibrationReader(
"yolov8n.onnx", "./calib_images"),
quant_format=QuantFormat.QDQ,
activation_type=QuantType.QInt8,
weight_type=QuantType.QInt8,
per_channel=True
)
print("INT8 model oluşturuldu: yolov8n_int8.onnx")
Optimize edilmiş modeli kaydet
import onnxruntime as ort
so = ort.SessionOptions()
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
# Optimize edilmiş modeli diske kaydet (bir kez çalıştır, sonra yükle)
so.optimized_model_filepath = "yolov8n_optimized.onnx"
session = ort.InferenceSession("yolov8n.onnx", sess_options=so)
# Artık yolov8n_optimized.onnx dosyası mevcut
# Sonraki çalıştırmalarda doğrudan bu dosyayı yükle (session başlatma hızlanır)
06 Cross-compile ve gömülü deploy — minimal build, arm64
Gömülü Linux hedefler için ORT'un minimal build sistemi, gereksiz EP'leri ve op'ları çıkararak küçük bir shared library üretir.
Minimal build sistemi
# aarch64 cross-compile toolchain kur
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# ORT kaynak kodu
git clone --depth 1 --branch v1.18.1 \
https://github.com/microsoft/onnxruntime
cd onnxruntime
# Minimal build: yalnızca CPU EP, QDQ INT8, temel op'lar
python tools/ci_build/build.py \
--build_dir ./build/arm64 \
--config Release \
--arm64 \
--cmake_extra_defines \
CMAKE_TOOLCHAIN_FILE=cmake/toolchains/aarch64-gcc.cmake \
onnxruntime_BUILD_UNIT_TESTS=OFF \
onnxruntime_MINIMAL_BUILD=ON \
onnxruntime_DISABLE_EXCEPTIONS=ON \
--parallel $(nproc) \
--skip_tests
# Sonuç: build/arm64/Release/libonnxruntime.so (~8 MB minimal)
ls -lh build/arm64/Release/libonnxruntime*
Gerekli op listesi oluşturma
# Model için gerekli op listesini çıkar
python tools/ci_build/build.py \
--build_dir ./build/required_ops \
--config Release \
--minimal_build extended \
--enable_reduced_operator_type_support \
--include_ops_by_model yolov8n.onnx
# Üretilen required_ops.config → minimal build'e dahil edilir
RPi hedefine kopyalama ve test
# Kütüphane ve başlıkları RPi'ye kopyala
scp build/arm64/Release/libonnxruntime.so pi@raspberrypi:/usr/local/lib/
scp -r include/onnxruntime/ pi@raspberrypi:/usr/local/include/
# RPi'de ldconfig güncelle
ssh pi@raspberrypi "sudo ldconfig"
# Uygulama derle (RPi'de doğrudan)
g++ -O2 -o infer ort_c_infer.c \
-I/usr/local/include/onnxruntime \
-L/usr/local/lib -lonnxruntime \
-Wl,-rpath,/usr/local/lib
./infer # test et
07 PyTorch/TF → ONNX dönüşüm
Model dönüşümü, ONNX ekosisteminin temel adımıdır. torch.onnx.export ve tf2onnx, farklı çerçevelerden ONNX formatına geçişi sağlar.
PyTorch → ONNX
import torch
from ultralytics import YOLO
# YOLOv8 nano modeli yükle
model = YOLO("yolov8n.pt")
# Yöntem 1: Ultralytics export (önerilen)
model.export(format="onnx", opset=17,
simplify=True,
dynamic=False,
imgsz=640)
# → yolov8n.onnx
# Yöntem 2: torch.onnx.export (manuel)
dummy_input = torch.randn(1, 3, 640, 640)
torch.onnx.export(
model.model,
dummy_input,
"yolov8n_manual.onnx",
opset_version=17,
input_names=["images"],
output_names=["output0"],
dynamic_axes={
"images": {0: "batch_size"},
"output0": {0: "batch_size"}
},
export_params=True
)
TensorFlow/Keras → ONNX
import tf2onnx
import tensorflow as tf
model = tf.keras.applications.MobileNetV2(weights="imagenet")
# tf2onnx ile dönüştür
input_signature = [tf.TensorSpec(
shape=[None, 224, 224, 3],
dtype=tf.float32,
name="input")]
model_proto, _ = tf2onnx.convert.from_keras(
model,
input_signature=input_signature,
opset=17,
output_path="mobilenetv2.onnx"
)
print("Dönüşüm tamamlandı")
Dönüşüm doğrulama
import onnx, onnxruntime as ort, numpy as np
# ONNX model doğrulama
model = onnx.load("yolov8n.onnx")
onnx.checker.check_model(model)
print("ONNX model geçerli")
# ORT ile çalıştırarak sayısal doğrulama
session = ort.InferenceSession("yolov8n.onnx")
inp = np.random.rand(1, 3, 640, 640).astype(np.float32)
out = session.run(None, {"images": inp})
print(f"Çıkış şekli: {out[0].shape}, min={out[0].min():.4f}")
08 Pratik — YOLOv8 nano RPi CM4 <50ms nesne tespiti
Raspberry Pi CM4 (4GB RAM, 4x Cortex-A72) üzerinde YOLOv8 nano ONNX modeli ile gerçek zamanlı nesne tespiti. Python ve C++ API karşılaştırması.
Pipeline mimarisi
USB Kamera (V4L2)
↓ 640×480 BGR
[cv2.VideoCapture]
↓
[Ön işleme: resize 640×640, normalize /255, NCHW]
↓
[ORT InferenceSession — CPU EP, 4 thread]
↓ (1, 84, 8400) float32
[NMS post-processing — onnxruntime + numpy]
↓
[Bounding box + label overlay]
↓
[cv2.imshow veya RTSP stream]
Python implementasyonu
import cv2, numpy as np, time
import onnxruntime as ort
CONF_THRESH = 0.5
IOU_THRESH = 0.45
COCO_CLASSES = ["person","bicycle","car","motorbike","bus","truck"] # kısaltılmış
# Session hazırlama
so = ort.SessionOptions()
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
so.intra_op_num_threads = 4
session = ort.InferenceSession("yolov8n.onnx", sess_options=so,
providers=["CPUExecutionProvider"])
def preprocess(frame):
img = cv2.resize(frame, (640, 640))
img = img[:, :, ::-1].astype(np.float32) / 255.0 # BGR→RGB, normalize
return img.transpose(2, 0, 1)[np.newaxis] # HWC→NCHW
def postprocess(output, orig_shape):
"""output: (1,84,8400) — YOLOv8 format"""
pred = output[0].T # (8400, 84)
boxes = pred[:, :4]
scores = pred[:, 4:].max(axis=1)
class_id = pred[:, 4:].argmax(axis=1)
mask = scores > CONF_THRESH
boxes, scores, class_id = boxes[mask], scores[mask], class_id[mask]
# xywh → xyxy, scale to original
h, w = orig_shape[:2]
cx, cy, bw, bh = boxes.T
x1 = (cx - bw/2) * w / 640
y1 = (cy - bh/2) * h / 480
x2 = (cx + bw/2) * w / 640
y2 = (cy + bh/2) * h / 480
return np.stack([x1,y1,x2,y2], axis=1), scores, class_id
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret: break
inp = preprocess(frame)
t0 = time.perf_counter()
out = session.run(None, {"images": inp})
dt = (time.perf_counter() - t0) * 1000
boxes, scores, cls = postprocess(out, frame.shape)
for (x1,y1,x2,y2), s, c in zip(boxes, scores, cls):
label = f"{COCO_CLASSES[c] if c < len(COCO_CLASSES) else c} {s:.2f}"
cv2.rectangle(frame, (int(x1),int(y1)), (int(x2),int(y2)), (0,255,0), 2)
cv2.putText(frame, label, (int(x1),int(y1)-5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1)
cv2.putText(frame, f"{dt:.1f}ms", (10,25),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,255), 2)
cv2.imshow("YOLOv8n ORT", frame)
if cv2.waitKey(1) == ord('q'): break
Python vs C++ karşılaştırması (RPi CM4)
| API | Session init | Çıkarım (ortalama) | Post-processing | Toplam FPS |
|---|---|---|---|---|
| Python (4t) | 2.1 s | 38 ms | 8 ms | ~22 FPS |
| C++ (4t) | 1.8 s | 34 ms | 3 ms | ~27 FPS |
| C++ + INT8 | 1.9 s | 22 ms | 3 ms | ~40 FPS |
YOLOv8n float32 ile Python/C++ API her iki durumda da 50ms hedefini geçer. INT8 QDQ quantization ile 22ms çıkarım elde edilir ve 50ms hedefinin altına inilir. Daha küçük giriş boyutu (416×416) kullanmak da gecikmeyi %20 azaltır.