ONNX i ONNX Runtime – po co to wszystko?
Format ONNX jako wspólny język dla modeli AI
Ekosystem uczenia maszynowego jest rozbity na wiele frameworków: PyTorch, TensorFlow, Keras, scikit-learn, XGBoost i kolejne biblioteki wyrastające jak grzyby po deszczu. Modele powstają w jednym środowisku, a później trzeba je uruchomiać w innym – często lżejszym, bardziej przewidywalnym i łatwiejszym do utrzymania w środowisku produkcyjnym. W tym miejscu pojawia się ONNX (Open Neural Network Exchange), czyli otwarty format wymiany modeli.
ONNX definiuje standardowy graf obliczeniowy (węzły – operacje, krawędzie – tensory) oraz zbiór operatorów (opset), które opisują typowe elementy modeli: konwolucje, warstwy gęste, ReLU, softmax, operacje na sekwencjach, a także klasyczne algorytmy ML. Dzięki temu model zapisany w ONNX nie „należy” już do PyTorcha czy TensorFlow – staje się niezależnym artefaktem, który da się wczytać w wielu różnych runtime’ach.
Najważniejsza korzyść: separacja treningu od inferencji. Trening można nadal prowadzić w ulubionym frameworku, z całym jego bagażem zależności, debugowaniem i eksperymentowaniem. Natomiast inferencja (czyli wykonywanie predykcji) może być realizowana przez wyspecjalizowany, zoptymalizowany silnik, który nie potrzebuje całego ciężaru frameworka naukowego.
ONNX Runtime jako wysokowydajny silnik inferencji
Format modelu to dopiero połowa układanki. Druga część to ONNX Runtime (ORT) – silnik inferencji rozwijany przez Microsoft oraz społeczność. Jego główna misja jest prosta: jak najszybciej i jak najtaniej wykonać graf obliczeniowy zapisany w ONNX, na możliwie różnych typach sprzętu.
ONNX Runtime:
- działa na CPU, korzystając z bibliotek typu oneDNN, MKL czy własnych optymalizacji,
- obsługuje GPU (CUDA, DirectML),
- potrafi wykorzystać akceleratory jak TensorRT czy Intel OpenVINO,
- ma warianty na mobile i edge (ORT Mobile, WebAssembly, WebGL),
- oferuje API dla wielu języków (Python, C/C++, C#, Java, JavaScript).
W praktyce oznacza to, że nie trzeba przepisywać modelu. Wystarczy wyeksportować go do ONNX, a następnie podpiąć ONNX Runtime jako bibliotekę w aplikacji (serwerowej, desktopowej, mobilnej czy webowej).
Typowe problemy bez ONNX Runtime
Inferencja bez wyspecjalizowanego silnika często kończy się na prostym „uruchomieniu modelu” bez refleksji nad wydajnością. Z czasem zaczynają się kłopoty:
- powolne predykcje – backend API w Pythonie z PyTorchem lub TensorFlow, działający w pojedynczym procesie, nie wykorzystuje w pełni CPU, a latencja dla jednego zapytania rośnie wraz z obciążeniem,
- ciężki runtime – cały framework naukowy w środowisku produkcyjnym obciąża pamięć, komplikuje zależności i utrudnia aktualizacje,
- problemy z portowaniem – model wytrenowany w PyTorch ma działać w aplikacji C# lub na małym urządzeniu ARM, gdzie instalacja pełnego PyTorcha jest nierealna lub ryzykowna.
ONNX Runtime rozwiązuje te problemy, umożliwiając uruchamianie tego samego modelu:
- na serwerze Linux z CPU,
- na maszynie z GPU NVIDIA,
- na urządzeniu edge z procesorem ARM,
- w przeglądarce przez WebAssembly.
Prosta mapa mentalna: format, silnik, provider
Łatwo się pogubić w nazwach, dlatego przydaje się prosta mapa:
- ONNX – format modelu, plik
.onnxzawierający graf obliczeniowy oraz parametry, - ONNX Runtime – silnik inferencji, biblioteka, która ładuje plik ONNX i wykonuje obliczenia,
- Execution Provider – „most” do konkretnego sprzętu (CPU, CUDA, TensorRT, DirectML, OpenVINO itd.), decydujący, które operacje i jak są wykonywane.
Model ONNX jest więc czymś w rodzaju „pliku wykonywalnego dla AI”, ONNX Runtime odpowiada roli „systemu operacyjnego dla modelu”, a Execution Provider przypomina „sterowniki” i mechanizmy akceleracji dla danego hardware’u.
Scenariusze, w których ONNX Runtime robi różnicę
Kilka realistycznych przykładów, gdzie ONNX Runtime pomaga wprost:
- Serwis API z modelem NLP – dotąd działał na Flasku z PyTorchem, jeden model BERT, duża latencja przy większym obciążeniu. Po eksporcie do ONNX i przejściu na ONNX Runtime (CPU lub GPU) latencja pojedynczego zapytania spada nawet kilkukrotnie, a zasoby są lepiej wykorzystane.
- Klasyfikacja obrazów na małym serwerze – mikroserwis w .NET musi klasyfikować zdjęcia, ale instalacja TensorFlow jest kłopotliwa. Ekosystem C# z ONNX Runtime wczytuje model z Keras w formacie ONNX i uruchamia go natywnie.
- Analiza wideo na urządzeniu edge – kamera przemysłowa z procesorem ARM i ograniczoną pamięcią musi wykonywać detekcję obiektów. ONNX Runtime w wersji Mobile z modelem skwantyzowanym (INT8) pozwala utrzymać real-time bez pełnego środowiska Pythonowego.
Ekosystem ONNX Runtime – architektura, komponenty i warianty
Szkic architektury: sesja, optymalizator grafu, providery
ONNX Runtime jest zbudowany modułowo. Kluczowe pojęcia:
- InferenceSession – centralny obiekt, który ładuje model ONNX, optymalizuje go i zarządza inferencją,
- Graph Optimizer – moduł, który upraszcza graf: łączy operacje (fusion), usuwa martwe gałęzie, stosuje stałe składanie (constant folding),
- Execution Providers – odpowiedzialne za wykonanie konkretnych operatorów na danym sprzęcie,
- Executors – wewnętrzne mechanizmy, które planują i wykonują poszczególne węzły grafu, wykorzystując wątki i pamięć.
Typowy przepływ:
- Ładowanie modelu ONNX z pliku lub z pamięci.
- Optymalizacja grafu: uproszczenia, fuzje, zmiany układu operatorów.
- Przydzielenie operatorów do dostępnych providerów (np. część na GPU, część na CPU).
- Wykonanie grafu dla konkretnych wejść – możliwe wielokrotnie, z różnymi danymi.
Duża część przyspieszenia inferencji wynika z tego, że ONNX Runtime wykonuje jednorazową, kosztowną optymalizację przy tworzeniu sesji, a nie za każdym wywołaniem predykcji. Dlatego sesję warto tworzyć rzadko, a korzystać z niej wielokrotnie.
Warianty ONNX Runtime: standard, mobile, web
Nie istnieje jeden „jedyny” ONNX Runtime. Pod wspólnym brandem kryje się kilka dystrybucji:
- Standardowy ONNX Runtime – biblioteka dla serwerów i desktopów, dostępna w wielu wariantach (CPU-only, GPU CUDA, DirectML, TensorRT, OpenVINO). Najczęstszy wybór w backendach API.
- ONNX Runtime Mobile – wariant zredukowany, pozwalający skompilować tylko podzbiór operatorów potrzebnych dla konkretnego modelu. Daje mniejszy rozmiar binariów, ważny dla Androida, iOS i lekkich urządzeń edge.
- ONNX Runtime Web – wersja działająca w przeglądarce, wykorzystująca WebAssembly (WASM) i WebGL/WebGPU. Umożliwia uruchamianie modeli AI po stronie klienta, bez wysyłania danych na serwer.
Dodatkowo ONNX Runtime integruje się z:
- DirectML – akceleracja GPU na Windowsie, szczególnie przy kartach AMD/Intel,
- TensorRT – kompilator i runtime od NVIDII, wyspecjalizowany w optymalizacji modeli na GPU,
- OpenVINO – narzędzia Intela do optymalizacji modeli dla CPU/VPUs/FPGA od tego producenta.
Obsługiwane języki i wybór pod stos technologiczny
ONNX Runtime jest napisany w C++, a oficjalne i nieoficjalne bindingi pozwalają korzystać z niego w wielu językach:
- Python – najwygodniejszy do eksperymentów, prototypowania i skryptów pomocniczych; dominujący w MLOps.
- C/C++ – gdy liczy się maksymalna wydajność i minimalne zależności, np. firmware, oprogramowanie low-latency.
- C# (.NET) – popularny wybór w firmach korzystających z ekosystemu Microsoft (aplikacje serwerowe, desktop, czasem mobile przez MAUI lub Xamarin).
- Java – integracja z systemami JVM, np. microserwisy w Spring, systemy big data.
- JavaScript/TypeScript – użycie przez ONNX Runtime Web po stronie przeglądarki, a także w Node.js.
Dobór języka do ONNX Runtime w praktyce jest prosty: warto pozostać w języku, w którym powstaje aplikacja produkcyjna. Model może powstać w Pythonie, ale inferencja może być realizowana z C# lub Java bez konieczności utrzymywania Pythonowego runtime’u.
Wersje ONNX, opset i kompatybilność
Modele ONNX korzystają z tzw. opsetów – zestawów operatorów o określonych wersjach. ONNX Runtime obsługuje tylko niektóre wersje opsetów, a poszczególne EP mogą mieć dodatkowe ograniczenia. To oznacza, że:
- przy eksporcie modelu trzeba świadomie zdecydować, jaki opset_version wykorzystać,
- przy aktualizacji ONNX Runtime warto sprawdzić listę obsługiwanych opsetów w dokumentacji,
- pewne operatory mogą zyskać optymalizacje dopiero w nowszym opsetcie.
Najprostsze podejście: stosować zalecany opset dla danej wersji ONNX Runtime oraz konkretnego execution providera. Jeśli pojawiają się problemy z kompatybilnością, warto zmniejszyć opset przy eksporcie lub zaktualizować runtime.
Co działa „z pudełka”, a co wymaga dodatkowej konfiguracji
Standardowa instalacja onnxruntime (CPU-only) zapewnia już:
- optymalizację grafu,
- wielowątkowość na CPU,
- współdzielenie zasobów między sesjami,
- profilowanie czasu wykonania operatorów.
Dodatkowe kroki są potrzebne, gdy:
- chcesz wykorzystać GPU – potrzebny jest pakiet
onnxruntime-gpulub dedykowany build z TensorRT, - celujesz w edge/mobile – przydaje się ONNX Runtime Mobile i budowanie skrojonej biblioteki,
- potrzebujesz integracji z OpenVINO, DirectML lub innym EP – wymagana jest wersja runtime’u z odpowiednim wsparciem oraz sterowniki.

Przygotowanie modelu – eksport do formatu ONNX
Eksport z PyTorch za pomocą torch.onnx
PyTorch ma wbudowany mechanizm eksportu do ONNX. Podstawowy schemat:
- Zdefiniowany i wytrenowany model PyTorch w klasie
nn.Module. - Stworzenie przykładowego wejścia (dummy input) z odpowiednim kształtem.
- Wywołanie
torch.onnx.exportz odpowiednimi parametrami.
Kluczowe parametry torch.onnx.export:
opset_version– wersja opsetu ONNX; zazwyczaj warto trzymać się rekomendacji ONNX Runtime,input_namesioutput_names– ułatwiają późniejsze korzystanie z modelu,dynamic_axes– jeśli model ma przyjmować zmienne rozmiary sekwencji, batch size itd.
Przykładowy fragment:
torch.onnx.export(
model,
dummy_input,
"model.onnx",
input_names=["input_ids", "attention_mask"],
output_names=["logits"],
dynamic_axes={"input_ids": {0: "batch"}, "attention_mask": {0: "batch"}, "logits": {0: "batch"}},
opset_version=17
)
Eksport z TensorFlow i Keras do ONNX
W świecie TensorFlow i Keras najczęściej używane jest narzędzie tf2onnx. Typowy przepływ:
- Masz model w formacie SavedModel lub
.h5. - Ładujesz go w TensorFlow / Keras.
- Konwertujesz do ONNX za pomocą skryptu lub linii komend
tf2onnx.convert.
Kluczowe kwestie:
- nie wszystkie operacje TensorFlow mają bezpośredni odpowiednik w ONNX,
Ograniczenia eksportu i strategie obejścia problemów
Eksport do ONNX rzadko jest całkowicie „magiczny”. Przy bardziej złożonych modelach pojawiają się nieobsługiwane operacje, niestandardowe warunki czy dynamiczne konstrukcje. Zamiast walczyć z narzędziem godzinami, lepiej podejść do tego systematycznie.
- Nietypowe operatory – jeśli tf2onnx czy torch.onnx sygnalizują brak wsparcia dla danego opu, najpierw sprawdź, czy nowsza wersja biblioteki go nie dodaje. Często aktualizacja rozwiązuje część problemów.
- Custom layers – niestandardowe warstwy w PyTorchu czy Kerasie można przedstawić jako kombinację prostszych, wspieranych operacji. Czasem opłaca się przepisać fragment modelu.
- Dynamiczna logika w forward – pętle
for, instrukcjeifzależne od danych lub budowanie grafu „w locie” bywają trudne do przełożenia na statyczny graf ONNX. Pomaga uproszczenie logiki lub rozbicie modelu na część neuronalną i otoczkę w kodzie aplikacji. - Operacje niedeterministyczne – np. losowe dropouty, sampling; często trzeba je zamienić na deterministyczne odpowiedniki lub wyłączyć na etapie eksportu (np.
model.eval()w PyTorchu).
Praktyczny sposób pracy: zaczynaj od najprostszego możliwego wariantu modelu, który przechodzi eksport, a potem stopniowo dodawaj elementy, obserwując, w którym miejscu konwersja się łamie. Ułatwia to zlokalizowanie „spornych” fragmentów.
Walidacja poprawności modelu ONNX
Sam fakt, że powstał plik .onnx, nie oznacza jeszcze sukcesu. Trzeba sprawdzić, czy model daje te same (lub bardzo podobne) wyniki co oryginał.
Podstawowe kroki walidacji:
- Wygeneruj kilka lub kilkanaście zestawów wejść testowych – najlepiej tych samych, których używasz do sanity checków w oryginalnym frameworku.
- Policz predykcje modelem źródłowym (PyTorch/TensorFlow) oraz modelem w ONNX Runtime.
- Porównaj wyniki: dla modeli regresyjnych różnice licz; dla klasyfikacji – top-1/top-k.
Drobne odchylenia są normalne, szczególnie po optymalizacjach czy zmianach typu liczbowego (np. FP32 → FP16). Krytyczne jest, żeby zmiany nie wpływały istotnie na decyzje biznesowe. Jeśli rozjazd jest duży, dobrze jest:
- na chwilę wyłączyć optymalizacje grafu w ONNX Runtime i sprawdzić, czy problem leży w konwersji, czy w optymalizatorze,
- przetestować różne wersje opsetu lub wersję
onnxruntime, - porównać pośrednie wyniki (np. aktywacje po konkretnych warstwach), by zawęzić źródło błędu.
Optymalizacje przed eksportem: prunowanie, kwantyzacja, uproszczenia
Model da się przyspieszyć nie tylko w ONNX Runtime, ale też na etapie przygotowania. Niewielkie zmiany architektury często dają lepszy efekt niż najbardziej wyszukane triki po stronie runtime’u.
- Prunowanie (pruning) – usuwanie mało istotnych wag lub całych kanałów/neuronuów. Zmniejsza liczbę operacji, czasem bez odczuwalnej utraty jakości. Po pruningu warto ponownie przetrenować (fine-tuning).
- Uproszczenie architektury – zastąpienie „ciężkich” bloków lżejszymi odpowiednikami (np. MobileNet zamiast ResNeT, DistilBERT zamiast pełnego BERT-a).
- Kwantyzacja post-training – zamiana FP32 na INT8 lub mieszane precyzje, często możliwa po konwersji do ONNX z użyciem narzędzi takich jak
onnxruntime-toolslubonnxruntime.quantization.
Wielu zespołom pomaga podejście iteracyjne: najpierw uruchomienie modelu „wprost” w ONNX Runtime, potem stopniowe włączanie pruningu czy kwantyzacji i obserwacja wpływu na SLA (czas odpowiedzi) oraz metryki jakości.
Pierwsze uruchomienie ONNX Runtime – od Hello World do wydajnej sesji
Instalacja i minimalny przykład w Pythonie
Python jest najłatwiejszym miejscem na pierwszy kontakt z ONNX Runtime, nawet jeśli produkcja działa później w innym języku.
Instalacja wariantu CPU:
pip install onnxruntime
Prosty przykład inferencji zakładający, że mamy plik model.onnx i pojedynczy tensor wejściowy:
import onnxruntime as ort
import numpy as np
# 1. Stworzenie sesji
sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
# 2. Przygotowanie wejścia
input_name = sess.get_inputs()[0].name
dummy_input = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 3. Uruchomienie inferencji
outputs = sess.run(None, {input_name: dummy_input})
print(outputs[0].shape)Taka „zabawka” szybko pokazuje, czy model działa i jaka jest orientacyjna struktura wyjścia. Kiedy to zadziała, można przejść do realnych danych i bardziej wymagającej konfiguracji.
Tworzenie i konfiguracja InferenceSession
Najwięcej możliwości kryje się w parametrach sesji. Dla prostych zastosowań wystarczy ścieżka do modelu, ale w praktyce przydaje się większa kontrola nad wątkami, poziomem optymalizacji i providerami.
Konfiguracja w Pythonie z użyciem SessionOptions:
import onnxruntime as ort
so = ort.SessionOptions()
so.intra_op_num_threads = 4 # wątki w obrębie operatora
so.inter_op_num_threads = 1 # wykonywanie niezależnych operatorów
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
sess = ort.InferenceSession("model.onnx", sess_options=so, providers=["CPUExecutionProvider"])Kilka praktycznych wskazówek:
- intra_op_num_threads dobrze jest dopasować do liczby rdzeni CPU dostępnych dla procesu; przy współdzieleniu maszyny z innymi usługami bywa rozsądne lekkie „niedoszacowanie”.
- inter_op_num_threads ma znaczenie szczególnie przy złożonych grafach, gdzie wiele operacji może być wykonywanych równolegle.
- graph_optimization_level ustaw na pełny poziom w środowiskach produkcyjnych; w trakcie debugowania czasem wygodniej zejść niżej, żeby łatwiej porównywać wyniki.
Jedna sesja czy wiele? Organizacja inferencji w aplikacji
Częste pytanie: czy dla każdego żądania HTTP tworzyć nową sesję? Nie ma takiej potrzeby, a wręcz to szkodzi wydajności. Tworzenie InferenceSession jest kosztowne, bo obejmuje wczytanie modelu, optymalizację grafu i inicjalizację pamięci.
Bezpieczniejsza praktyka:
- twórz jedną sesję na proces (np. w czasie startu aplikacji),
- wielokrotnie wywołuj
sess.runz różnymi wejściami, - dbaj o to, żeby model był bezstanowy (co jest normą dla większości architektur).
W środowiskach o dużej przepustowości można stosować kilka sesji per proces (np. osobne dla różnych modeli) lub kilka procesów, każdy z własną sesją, a na wierzchu prosty load-balancer. Kluczowe jest obserwowanie, gdzie faktycznie pojawia się wąskie gardło: CPU, GPU, pamięć czy I/O.
Mapowanie nazw wejść i wyjść
Wielu osobom przeszkadza początkowe „zgadywanie” nazw wejść i wyjść w ONNX Runtime. Szybki sposób na ich podejrzenie:
sess = ort.InferenceSession("model.onnx")
print("Inputs:")
for inp in sess.get_inputs():
print(inp.name, inp.shape, inp.type)
print("Outputs:")
for out in sess.get_outputs():
print(out.name, out.shape, out.type)Taki fragment dobrze jest mieć pod ręką przy integracji z nowym modelem. Dzięki temu łatwo zobaczyć, jakiej struktury danych oczekuje runtime i jak będzie wyglądał rezultat.
Profilowanie i diagnostyka wydajnościowa
Jeśli inferencja jest wolniejsza niż się spodziewasz, warto sprawdzić, które operatory faktycznie zajmują najwięcej czasu. ONNX Runtime ma wbudowane profilowanie.
so = ort.SessionOptions()
so.enable_profiling = True
sess = ort.InferenceSession("model.onnx", sess_options=so)
# kilka wywołań inferencji
for _ in range(10):
_ = sess.run(None, {input_name: dummy_input})
profile_file = sess.end_profiling()
print("Profil zapisany w:", profile_file)Plik z profilem można przeanalizować ręcznie lub w narzędziach do wizualizacji. Szybko widać wtedy, które opy dominują i czy opłaca się np. zastosować inny execution provider lub zmienić architekturę.

Execution Providers – efektywne wykorzystanie CPU, GPU i akceleratorów
Model wykonania z wieloma providerami
Execution Provider (EP) to warstwa, która odpowiada za wykonanie operatorów na konkretnym typie sprzętu: CPU, GPU, NPU, FPGA czy wyspecjalizowanym kompilatorze (TensorRT, OpenVINO). ONNX Runtime może używać kilku providerów naraz.
Kolejność providerów ma znaczenie – runtime próbuje przypisać operator do pierwszego providera, który go obsługuje, a jeśli się nie uda, przechodzi do następnego. Typowa konfiguracja w Pythonie:
providers = [
("CUDAExecutionProvider", {"cudnn_conv_algo_search": "EXHAUSTIVE"}),
"CPUExecutionProvider",
]
sess = ort.InferenceSession("model.onnx", providers=providers)Jeśli część operatorów nie ma implementacji na GPU (lub innym akceleratorze), „spadną” one na CPU. Nie trzeba ich ręcznie rozdzielać, chociaż czasem taka świadoma fragmentacja grafu też ma sens.
CPUExecutionProvider – wyciśnięcie maksimum z procesora
Dla wielu aplikacji odpowiednio skonfigurowany CPU wystarcza do osiągnięcia akceptowalnych czasów odpowiedzi. W szczególności:
- modele o niewielkiej liczbie parametrów,
- systemy wsadowe, gdzie liczy się przepustowość bardziej niż pojedyncza latencja,
- maszyny bez akceleratorów (np. sporo serwerów on-prem).
Kilka wskazówek dla CPU:
- dobierz sensownie liczbę wątków (
intra_op_num_threads,inter_op_num_threads) – za dużo wątków może prowadzić do nadmiernego przełączania kontekstu i spadku wydajności, - sprawdź wpływ batchowania – zamiast pojedynczych żądań jedno po drugim, czasem opłaca się spakować np. 8–16 próbek w jeden batch,
- rozważ CPU z instrukcjami wektorowymi (AVX2, AVX-512) – wiele operatorów matematycznych korzysta z nich automatycznie.
CUDAExecutionProvider – wykorzystanie GPU NVIDII
Jeśli masz do dyspozycji GPU NVIDII, CUDAExecutionProvider jest pierwszym naturalnym wyborem. Umożliwia wykonywanie większości standardowych operatorów na karcie graficznej, co drastycznie przyspiesza duże modele, szczególnie CNN i transformery.
Instalacja pakietu GPU:
pip install onnxruntime-gpuNastępnie konfiguracja:
import onnxruntime as ort
sess = ort.InferenceSession(
"model.onnx",
providers=[
("CUDAExecutionProvider", {
"device_id": 0,
"arena_extend_strategy": "kNextPowerOfTwo",
"gpu_mem_limit": 2 * 1024 * 1024 * 1024, # 2 GB
}),
"CPUExecutionProvider",
],
)Kilka praktycznych obserwacji:
- przy małych batchach overhead transferu danych CPU↔GPU może zjadać korzyści z akceleracji,
- w ciężkich modelach transformatorowych warto testować większe batch size, o ile pozwala na to pamięć GPU,
- profilowanie pokazuje, czy wąskim gardłem jest samo GPU, czy może kod hosta (np. przygotowanie danych).
TensorRTExecutionProvider – maksymalna optymalizacja na GPU NVIDII
TensorRT to specjalistyczny kompilator i runtime do modeli głębokiego uczenia na GPU NVIDII. W połączeniu z ONNX Runtime potrafi jeszcze bardziej skrócić czas inferencji niż czysta CUDA, szczególnie po kwantyzacji do FP16/INT8.
Typowy scenariusz użycia:
- przygotowanie modelu ONNX,
- instalacja ONNX Runtime z obsługą TensorRT (osobna paczka lub build),
- konfiguracja providera
TensorRTExecutionProvideri ewentualne włączenie FP16/INT8.
Przykładowa konfiguracja:
providers = [
("TensorrtExecutionProvider", {
"trt_fp16_enable": True,
"trt_int8_enable": False,
"trt_max_workspace_size": 1 << 30, # 1 GB
}),
"CUDAExecutionProvider",
"CPUExecutionProvider",
]
sess = ort.InferenceSession("model.onnx", providers=providers)TensorRT bywa bardziej wrażliwy na szczegóły modelu (np. nietypowe operatory, niestandardowe kształty). Jeśli integracja sprawia kłopot, można użyć CUDAExecutionProvider jako „bezpiecznej przystani” i przeprowadzić testy porównawcze.
OpenVINOExecutionProvider – przyspieszanie modeli na CPU i iGPU Intela
OpenVINOExecutionProvider dobrze sprawdza się na maszynach z procesorami Intela oraz zintegrowaną grafiką (iGPU). Często daje zysk nawet wtedy, gdy nie korzysta z zewnętrznego akceleratora – samo dopasowanie grafu do bibliotek Intela bywa odczuwalne.
Instalacja (przykładowa paczka, nazwy mogą się zmieniać wraz z wersjami):
pip install onnxruntime-openvinoPrzykładowa konfiguracja sesji:
import onnxruntime as ort
providers = [
("OpenVINOExecutionProvider", {
"device_type": "CPU_FP32", # inne: "GPU_FP16", "GPU", "CPU_FP16"
"num_of_threads": 4,
}),
"CPUExecutionProvider",
]
sess = ort.InferenceSession("model.onnx", providers=providers)Przy pierwszym uruchomieniu OpenVINO może kompilować model – to trwa dłużej, ale rezultat jest cache’owany. W aplikacjach serwerowych dobrze jest „rozgrzać” model jednym wywołaniem po starcie procesu, żeby użytkownicy nie natknęli się na tę pierwszą, wolniejszą inferencję.
Typowy scenariusz: serwer z CPU Intela, na którym działa kilka usług. Zastosowanie OpenVINOExecutionProvider pozwala przyspieszyć modele CV lub klasyfikatory tekstu bez inwestycji w osobne GPU, a jednocześnie utrzymać neutralne zużycie pamięci.
CoreMLExecutionProvider – przyspieszenie na urządzeniach Apple
Na macOS i iOS ONNX Runtime potrafi korzystać z Core ML jako warstwy wykonawczej. To sposób na użycie GPU/Neural Engine w urządzeniach Apple bez pisania kodu natywnego.
Przykładowa konfiguracja:
import onnxruntime as ort
providers = [
("CoreMLExecutionProvider", {
"coreml_flags": 0, # dodatkowe flagi, np. dla precyzji
}),
"CPUExecutionProvider",
]
sess = ort.InferenceSession("model.onnx", providers=providers)W aplikacjach desktopowych na macOS można w ten sposób uruchamiać modele trenowane na innych platformach i korzystać z lokalnej akceleracji, bez przenoszenia całej logiki do Swift/Objective‑C. Dobrze działa to przy przetwarzaniu obrazu „on device”, np. filtrach wideo czy klasyfikacji scen.
OrtExtensions i niestandardowe operatory
Przy integracji z akceleratorami czasem pojawia się obawa: model zawiera niestandardowe operatory, których dany execution provider nie zna. Zwykle kończy się to spadkiem na CPU albo błędem ładowania. ONNX Runtime oferuje jednak moduł onnxruntime-extensions, który uzupełnia zestaw operatorów o często spotykane funkcje (np. tekstowe, przetwarzanie obrazów).
Instalacja:
pip install onnxruntime-extensionsUżycie z sesją:
from onnxruntime_extensions import get_library_path
import onnxruntime as ort
so = ort.SessionOptions()
so.register_custom_ops_library(get_library_path())
sess = ort.InferenceSession(
"model.onnx",
sess_options=so,
providers=["CPUExecutionProvider"]
)Jeśli model zawiera operatory znane rozszerzeniom, nie trzeba ich przepisywać ani ręcznie „wklejać” do grafu. Dla części akceleratorów (np. TensorRT) nietypowe operatory i tak skończą na CPU, ale reszta grafu wciąż może być wykonana na GPU, więc sumaryczny zysk pozostaje.
Dynamiczna zmiana providerów i fallback w kodzie aplikacji
W środowiskach, gdzie sprzęt może się zmieniać (różne typy maszyn w klastrze, różne wersje sterowników), przydaje się elastyczna inicjalizacja providerów. Zamiast „na sztywno” zakładać obecność GPU, można spróbować kilku konfiguracji po kolei i wybrać pierwszą działającą.
import onnxruntime as ort
def create_session_with_fallback(model_path: str) -> ort.InferenceSession:
candidates = [
["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"],
["CUDAExecutionProvider", "CPUExecutionProvider"],
["CPUExecutionProvider"],
]
last_error = None
for providers in candidates:
try:
return ort.InferenceSession(model_path, providers=providers)
except Exception as e:
last_error = e
raise RuntimeError(f"Nie udało się utworzyć sesji: {last_error}")
sess = create_session_with_fallback("model.onnx")Taki wzorzec pomaga uniknąć sytuacji, w której cały serwis się wywraca po przeniesieniu kontenera na maszynę bez GPU. Z perspektywy użytkownika efekt jest prosty: aplikacja nadal działa, a różnica polega jedynie na czasie odpowiedzi.
ONNX Runtime na urządzeniach Edge
ONNX Runtime dobrze wpisuje się w scenariusze edge: kamery, bramy IoT, małe serwery w sklepach czy fabrykach. Główne wyzwanie to ograniczone zasoby – CPU, pamięć, a czasem brak stałego połączenia z siecią.
ONNX Runtime Mobile – odchudzony runtime na urządzenia mobilne
ONNX Runtime Mobile to specjalny wariant biblioteki zbudowany „pod” konkretny model. Zamiast wozić ze sobą implementacje wszystkich operatorów, zawiera tylko te, które są faktycznie używane. Dzięki temu biblioteka może być kilkukrotnie mniejsza.
Proces składa się z kilku kroków:
- przygotowanie modelu ONNX (docelowego, po optymalizacjach),
- wygenerowanie konfiguracji builda mobilnego (lista operatorów i typów),
- zbudowanie ONNX Runtime Mobile dla danej platformy (Android/iOS),
- integracja z aplikacją natywną (JNI/NDK, Swift/Objective‑C) lub z warstwą C# w przypadku Xamarin/MAUI.
Przy projektach, w których liczy się rozmiar APK/IPA i szybkość startu aplikacji, taka „dietetyczna” wersja runtime’u zmniejsza tarcie przy publikacji w sklepach i aktualizacjach dla użytkowników.
ARM, Jetson i małe komputery jednopłytkowe
Na urządzeniach typu Raspberry Pi, NVIDIA Jetson czy innych SBC z architekturą ARM ONNX Runtime działa natywnie, o ile biblioteka została zbudowana na odpowiednią architekturę. W wielu dystrybucjach Linuksa dostępne są gotowe paczki, ale w krytycznych aplikacjach lepiej zbudować runtime samodzielnie, z wybranymi tylko komponentami.
Przykładowy schemat dla Jetsona:
- model ONNX zredukowany pod docelową rozdzielczość wejścia i typ danych (FP16),
TensorRTExecutionProviderjako główny provider,- CPUExecutionProvider jako fallback dla kilku rzadkich operatorów,
- batch size = 1, ale możliwe pipeline’owanie inferencji na kilku strumieniach, jeśli obrazów jest więcej.
Z praktyki: w systemach wizyjnych w zakładach przemysłowych dobrą strategią bywa trzymanie mniejszego, szybszego modelu na urządzeniu edge (do filtracji wstępnej), a ciężkiego modelu w chmurze – tylko dla mniej oczywistych przypadków.
Zarządzanie pamięcią i wielkością modeli na Edge
Na małych urządzeniach ograniczeniem często nie jest sam czas inferencji, ale pamięć RAM i rozmiar modelu na dysku. Zamiast od razu sięgać po nowy sprzęt, można sięgnąć po kilka prostych technik:
- pruning – usunięcie neuronów lub kanałów o małym wkładzie w wynik,
- distillation – trenowanie mniejszego „ucznia” na wyjściach dużego „nauczyciela”,
- kwantyzacja statyczna – przejście z FP32 do INT8 przy użyciu zbioru kalibracyjnego,
- kwantyzacja dynamiczna – lżejsza forma, dobra np. dla modeli NLP, często bez ponownego trenowania.
Po każdej z takich operacji można ponownie wyeksportować model do ONNX i sprawdzić, jak zmienił się czas inferencji na docelowym urządzeniu. Niedogodnością bywa to, że trzeba balansować między jakością a wydajnością, ale przy sensownym doborze danych testowych da się to kontrolować.
ONNX Runtime w kontenerach na Edge
Coraz częściej urządzenia edge uruchamiają kontenery (Docker, podzbiory Kubernetesa). ONNX Runtime dobrze się w to wpisuje, o ile zadbamy o kilka szczegółów:
- obraz bazowy z odpowiednią wersją sterowników i bibliotek (CUDA, TensorRT, OpenVINO),
- limit CPU/memory w orchestratorze dopasowany do realnych potrzeb modelu,
- odpowiednia konfiguracja wątków, żeby nie „zadławić” całego węzła.
Dobrą praktyką jest trzymanie osobnych obrazów dla wersji CPU‑only i GPU‑enabled, z wykrywaniem dostępnego sprzętu przy starcie kontenera i wyborem providerów na tej podstawie. Ułatwia to przenoszenie aplikacji między różnymi klasami urządzeń bez modyfikacji kodu inferencji.
Zaawansowane techniki optymalizacji w ONNX Runtime
Kwantyzacja z użyciem narzędzi ONNX Runtime
ONNX Runtime oferuje własny zestaw narzędzi do kwantyzacji modeli ONNX. Dla wielu modeli NLP i CV przejście na INT8 daje znaczące przyspieszenie, przy spadku jakości akceptowalnym biznesowo.
Przykład kwantyzacji dynamicznej w Pythonie:
from onnxruntime.quantization import quantize_dynamic, QuantType
model_fp32 = "model_fp32.onnx"
model_int8 = "model_int8.onnx"
quantize_dynamic(
model_input=model_fp32,
model_output=model_int8,
weight_type=QuantType.QInt8,
)Dynamiczna kwantyzacja jest szybka, bo nie wymaga zbioru kalibracyjnego. Jeśli jednak celem jest maksimum wydajności przy zachowaniu jak najlepszej jakości, lepiej użyć kwantyzacji statycznej, z przejściem po reprezentatywnym zbiorze danych.
Skracanie ścieżki danych – pre‑ i post‑processing w tym samym procesie
Częsty błąd w aplikacjach produkcyjnych: dane wejściowe są przygotowywane w jednym procesie, następnie wysyłane przez sieć (lub IPC) do procesu inferencji, a na koniec wynik jest odsyłany z powrotem. Każde takie przejście to opóźnienie i dodatkowa alokacja pamięci.
Lepszy wariant:
- pre‑processing (np. resize, normalizacja) w tym samym procesie, tuż przed wywołaniem
sess.run, - post‑processing (np. NMS, dekodowanie tokenów) również lokalnie,
- przekazywanie do ONNX Runtime już gotowych tensorów w odpowiednich typach danych.
Jeśli pre‑processing wykorzystuje biblioteki GPU (np. CUDA, OpenCV z obsługą GPU), można przekazywać dane do ONNX Runtime z wykorzystaniem pamięci współdzielonej na GPU, ograniczając kopiowanie między hostem a urządzeniem. W API C/C++ jest to bardziej elastyczne niż w Pythonie, ale i tam da się zredukować liczbę konwersji typów.
Batching i kolejkowanie żądań
Przy serwisach o dużej liczbie równoległych żądań sensowne batchowanie daje spory zysk, szczególnie na GPU i w scenariuszach edge z kilkoma kamerami. Zamiast wywoływać model osobno dla każdego obrazu, można łączyć je w paczki.
Prosty przykład: kolejka w pamięci, do której trafiają żądania, a osobny wątek „zbiera” je przez np. 5 ms lub do osiągnięcia maksymalnego rozmiaru batcha:
import threading
import queue
import time
import onnxruntime as ort
import numpy as np
request_q = queue.Queue()
sess = ort.InferenceSession("model.onnx", providers=["CUDAExecutionProvider"])
input_name = sess.get_inputs()[0].name
def worker():
while True:
batch = []
futures = []
# zbieranie żądań przez krótki czas
start = time.time()
while (time.time() - start) < 0.005 and len(batch) < 16:
try:
inp, fut = request_q.get(timeout=0.001)
batch.append(inp)
futures.append(fut)
except queue.Empty:
break
if not batch:
continue
batch_arr = np.stack(batch, axis=0)
outputs = sess.run(None, {input_name: batch_arr})[0]
for out, fut in zip(outputs, futures):
fut.set_result(out)
threading.Thread(target=worker, daemon=True).start()
Taki schemat wymaga odrobinę więcej kodu organizacyjnego (np. obiektów typu Future), lecz w zamian uzyskuje się lepsze wykorzystanie GPU i stabilniejsze czasy odpowiedzi przy rosnącym obciążeniu.
Optymalizacje grafu: fusion, eliminacja martwych gałęzi, layout
ONNX Runtime ma wbudowane optymalizatory grafu, ale czasem opłaca się jeszcze przed eksportem modelu ułatwić im pracę. Przykłady:
- usunięcie nieużywanych wyjść (np. pomocniczych tensorów diagnostycznych),
- spłaszczenie nadmiarowych warstw
Identity,Reshape, podmianaBatchNormna równoważne operacje, - użycie stałych wymiarów tam, gdzie to możliwe, zamiast w pełni dynamicznych kształtów.
Im prostszy i bardziej „kanoniczny” graf ONNX, tym łatwiej execution providerom zastosować swoje wewnętrzne optymalizacje (fusion, re‑ordering, lepsze wykorzystanie pamięci tymczasowej).
Asynchroniczna inferencja i wielowątkowość
Samo sess.run jest wywołaniem blokującym. Nic jednak nie stoi na przeszkodzie, żeby obudować je w asynchroniczny interfejs i uruchamiać równolegle z logiką aplikacji. Dwa typowe podejścia:
- oddzielny wątek/worker do inferencji, jak w przykładzie z batchowaniem,
Najczęściej zadawane pytania (FAQ)
Czym jest ONNX Runtime i do czego służy?
ONNX Runtime to wysokowydajny silnik inferencji do uruchamiania modeli zapisanych w formacie ONNX. Jego zadanie jest proste: jak najszybciej i jak najtaniej wykonać obliczenia modelu na dostępnym sprzęcie – od CPU, przez GPU, po urządzenia edge i przeglądarkę.
Dzięki ONNX Runtime możesz trenować model w dowolnym frameworku (np. PyTorch, TensorFlow), wyeksportować go do ONNX, a potem ładować i wykonywać w lekkiej bibliotece produkcyjnej, bez dźwigania całego środowiska naukowego Pythona.
Jaka jest różnica między ONNX a ONNX Runtime?
ONNX to format pliku – standard zapisu modeli AI. Taki plik .onnx zawiera graf obliczeniowy (warstwy, operacje) i parametry modelu. Nie wykonuje sam z siebie żadnych obliczeń, jest raczej „plikem wykonywalnym” dla sztucznej inteligencji.
ONNX Runtime to biblioteka, która ten plik ładuje i uruchamia. Optymalizuje graf, przydziela operacje do CPU/GPU i zarządza inferencją. Można patrzeć na to tak: ONNX to „program”, ONNX Runtime to „system operacyjny”, a Execution Provider to „sterowniki” do konkretnego sprzętu.
Kiedy warto użyć ONNX Runtime zamiast pozostawać przy PyTorch/TensorFlow?
ONNX Runtime ma sens wszędzie tam, gdzie model ma działać produkcyjnie, a nie tylko w notatniku do eksperymentów. Typowe sytuacje:
- backend API w Pythonie zaczyna mieć za dużą latencję i zużycie pamięci, bo cały PyTorch/TensorFlow siedzi w procesie,
- model ma zostać wbudowany w aplikację .NET, C++ lub Java, gdzie instalacja pełnego frameworka ML byłaby ciężka lub niestabilna,
- potrzebujesz uruchamiać model na urządzeniu edge, mobilnym lub w przeglądarce.
Dla wielu zespołów typowy schemat to: trening w PyTorch/TensorFlow, eksport do ONNX, a potem inferencja przez ONNX Runtime w docelowej aplikacji.
Na jakim sprzęcie działa ONNX Runtime (CPU, GPU, edge, przeglądarka)?
ONNX Runtime obsługuje bardzo szerokie spektrum środowisk. Na serwerach i desktopach może korzystać z CPU (m.in. oneDNN, MKL) oraz GPU przez CUDA, DirectML, TensorRT czy OpenVINO. Dzięki Execution Providers możesz dobrać akcelerację pod konkretny typ sprzętu.
Dostępne są też warianty na urządzenia z ograniczonymi zasobami oraz front-end:
- ONNX Runtime Mobile – odchudzona wersja pod Androida, iOS i lekkie urządzenia ARM,
- ONNX Runtime Web – działający w przeglądarce przez WebAssembly i WebGL/WebGPU, bez potrzeby wysyłania danych na serwer.
Jakie języki programowania są wspierane przez ONNX Runtime?
Silnik jest napisany w C++, ale ma oficjalne i nieoficjalne bindingi do wielu języków. Najczęściej używane to Python (skrypty, MLOps), C/C++ (systemy low-latency), C#/.NET (aplikacje biznesowe, serwery), Java (systemy JVM, microserwisy) oraz JavaScript/TypeScript (przeglądarka i Node.js).
Jeśli masz już istniejącą aplikację, zwykle możesz dobrać odpowiednie API ONNX Runtime bez przepisywania całego systemu na inny język – to mocno obniża barierę wdrożenia modeli AI.
Jak ONNX Runtime przyspiesza inferencję modeli?
Przy tworzeniu sesji inferencyjnej ONNX Runtime jednorazowo optymalizuje graf modelu: łączy operacje w większe bloki (fusion), usuwa martwe fragmenty, wykonuje z góry stałe obliczenia (constant folding) i planuje wykonanie z użyciem wielu wątków.
Dodatkowo, przez Execution Providers, deleguje fragmenty obliczeń na najszybsze dostępne zasoby – np. warstwy konwolucyjne na GPU, a prostsze operacje na CPU. Sesję warto tworzyć rzadko i używać wielokrotnie, żeby „spłacić” koszt optymalizacji przy pierwszym uruchomieniu.
Czy mogę użyć ONNX Runtime na urządzeniach mobilnych i edge?
Tak. ONNX Runtime Mobile został zaprojektowany właśnie z myślą o Androidzie, iOS i małych urządzeniach ARM. Pozwala zbudować minimalny runtime zawierający tylko operatorzy potrzebne dla danego modelu, co zmniejsza rozmiar binariów i zapotrzebowanie na pamięć.
W praktyce oznacza to, że model wytrenowany np. w PyTorch można skwantyzować (np. do INT8), wyeksportować do ONNX i uruchomić lokalnie na kamerze przemysłowej czy smartfonie, zachowując real-time bez instalowania pełnego środowiska Python + framework ML.






