ONNX Runtime w produkcji może skrócić czas inferencji, ograniczyć zużycie pamięci i zmniejszyć liczbę potrzebnych instancji chmurowych, ale sama konwersja modelu do formatu ONNX nie gwarantuje poprawy. Wynik zależy od operatorów, wybranego Execution Provider, sposobu przygotowania danych, długości kolejki oraz tego, czy mierzona jest średnia latencja, czy rzeczywiste p95 i p99.
Najczęstszy błąd polega na leczeniu wolnej inferencji kolejnymi instancjami GPU. Jeżeli problemem jest narzut frameworka, nieefektywny graf, kopiowanie danych między CPU i GPU albo źle ustawiony batching, skalowanie zwiększa rachunek, lecz nie usuwa przyczyny. Decyzję o wdrożeniu ONNX Runtime trzeba więc oprzeć na porównaniu wariantów, kontroli jakości predykcji i pełnym koszcie obsługi żądania. [4]
Wąskie gardła produkcyjnej inferencji: dlaczego domyślne środowiska frameworków zawyżają rachunki za chmurę
Środowisko treningowe nie jest automatycznie dobrym serwerem predykcji
PyTorch i TensorFlow są projektowane przede wszystkim jako rozbudowane środowiska do budowania, trenowania i debugowania modeli. Zapewniają autograd, narzędzia do eksperymentów, obsługę wielu typów warstw oraz elastyczne mechanizmy wykonywania grafu. W produkcyjnym servingiem potrzebny jest jednak zwykle znacznie węższy zestaw funkcji: wczytanie modelu, przygotowanie wejścia, wykonanie obliczeń, postprocessing i zwrócenie odpowiedzi.
Uruchomienie modelu bezpośrednio w frameworku może oznaczać dodatkowy narzut pamięci, inicjalizacji i zarządzania grafem. Nie zawsze jest on duży, a w wielu przypadkach pozostaje akceptowalny ze względu na prostotę wdrożenia. Problem pojawia się wtedy, gdy model wykonuje stosunkowo krótkie obliczenia, lecz obsługa procesu, konwersja typów, przenoszenie tensorów i synchronizacja urządzeń zajmują porównywalnie dużo czasu.
Format ONNX oddziela reprezentację modelu od konkretnego frameworka treningowego. ONNX Runtime może przejąć wykonanie grafu i zastosować optymalizacje, takie jak eliminowanie zbędnych operacji, łączenie wybranych węzłów czy dobór implementacji operatorów pod określony procesor albo akcelerator. Nie oznacza to, że każdy model po eksporcie będzie szybszy. Konwersja jest początkiem pomiaru, a nie końcem optymalizacji. [1]
Średnia latencja ukrywa problemy z p99
W systemie produkcyjnym średni czas odpowiedzi bywa mylący. Kilka szybkich żądań może obniżyć średnią, mimo że część użytkowników regularnie czeka wielokrotnie dłużej. Dla API obsługującego rekomendacje, klasyfikację dokumentów lub moderację treści istotne są co najmniej p50, p95 i p99, osobno dla preprocessingu, inferencji i postprocessingu.
Niestabilność p99 może wynikać z przełączania wątków, konkurencji o pamięć, automatycznego skalowania, zimnego startu, przypadkowego mieszania dużych i małych próśb w jednej kolejce albo synchronizacji CPU z GPU. Na GPU dodatkowym problemem jest niewykorzystanie urządzenia: proces posiada drogą kartę, ale większość czasu spędza na oczekiwaniu na dane przygotowywane przez CPU.
Przed zmianą runtime’u trzeba ustalić, co dokładnie jest mierzone. Minimalny zestaw obejmuje rozmiar wejścia, kształt tensorów, długość sekwencji, batch size, czas oczekiwania w kolejce, czas kopiowania danych, czas wykonania grafu oraz czas serializacji odpowiedzi. Bez tego łatwo uznać za sukces zmianę, która przyspiesza samą funkcję inferencji, ale nie poprawia czasu odpowiedzi całego endpointu.
GPU nie zawsze oznacza niższy koszt obsługi żądania
Instancja GPU może być opłacalna przy dużym ruchu, dużych modelach i wysokiej równoległości. Przy małej liczbie żądań, nieregularnym ruchu albo niewielkim modelu koszt bezczynnego akceleratora może jednak przewyższać korzyść z krótszego czasu obliczeń. Szczególnie widoczne jest to w usługach działających przez całą dobę, lecz otrzymujących żądania tylko w krótkich oknach.
Punkt kontrolny: bilans kosztów powinien zestawiać cenę całej instancji, zajętość CPU i GPU, pamięć, przepustowość sieci oraz liczbę poprawnie obsłużonych żądań. Sam procent wykorzystania GPU nie wystarcza. Karta może być zajęta, ale system nadal może mieć zbyt wysoką latencję z powodu wąskiego gardła w wejściu lub wyjściu.
Sygnał ostrzegawczy stanowi sytuacja, w której każde pogorszenie czasu odpowiedzi prowadzi do uruchomienia kolejnych instancji z akceleratorem, bez sprawdzenia grafu obliczeniowego i ścieżki danych. Skalowanie horyzontalne jest narzędziem obsługi ruchu, nie substytutem profilowania.
Gdzie kończy się sens zwiększania zasobów
Jeżeli bazowy czas obliczeń tensora jest krótki, a resztę stanowią narzuty frameworka, kopiowanie danych i oczekiwanie na synchronizację, dodatkowe GPU nie rozwiąże problemu proporcjonalnie do kosztu. Podobnie zwiększenie liczby vCPU nie pomoże, gdy model jest ograniczony przepustowością pamięci albo gdy pojedyncze żądanie blokuje się na operacji wykonywanej sekwencyjnie.
Jeśli narzut środowiska przekracza bazowy czas obliczeń, pierwszym krokiem powinna być optymalizacja grafu i ścieżki wykonania. Jeśli natomiast model jest duży, dobrze zbatchowany, a GPU stale pracuje blisko swojej efektywnej przepustowości, migracja na CPU może pogorszyć zarówno latencję, jak i koszt. Kryterium nie brzmi więc „CPU czy GPU”, tylko „który wariant daje wymaganą latencję przy akceptowalnym koszcie jednostkowym”.
Zestawienie wariantów architektury inferencyjnej dla modeli ML
Wariant A: domyślny serving frameworka
Wariant oparty na TorchServe, TensorFlow Serving albo własnym serwerze korzystającym bezpośrednio z PyTorch lub TensorFlow jest najkrótszą drogą od zakończenia walidacji do produkcji. Model pozostaje w macierzystym formacie, a zespół nie musi rozwiązywać problemów z eksportem operatorów, dynamicznymi kształtami czy zgodnością wersji ONNX.
Największą zaletą jest zgodność. Jeżeli model używa niestandardowych warstw, funkcji kontrolnych lub operatorów specyficznych dla frameworka, bezpośredni serving zwykle wymaga mniej zmian. Łatwiejsze bywa również debugowanie różnic między treningiem a inferencją, ponieważ obie ścieżki korzystają z tego samego ekosystemu.
Ograniczenia pojawiają się przy dużej skali. Ciężkie zależności zwiększają rozmiar obrazu kontenera i czas startu, zużycie RAM oraz złożoność aktualizacji. Nie każdy graf jest automatycznie fuzowany, a optymalizacje zależne od konkretnego sprzętu mogą wymagać dodatkowej konfiguracji. W rezultacie wariant A często jest dobrym punktem odniesienia, lecz niekoniecznie dobrym wariantem docelowym. [5]
- Plusy: szybkie wdrożenie, pełna zgodność z frameworkiem, małe ryzyko błędów konwersji;
- Minusy: większy narzut pamięci i zależności, ograniczona przenośność, często słabsza efektywność kosztowa;
- Dla kogo: dla małego ruchu, prototypu produkcyjnego, modelu z niestandardowymi operatorami albo zespołu, który nie może jeszcze utrzymywać osobnej ścieżki eksportu.
Wariant B: ONNX Runtime na CPU z kwantyzacją INT8
W tym wariancie model jest eksportowany do ONNX, a wykonanie przejmuje ONNX Runtime z CPU Execution Provider. Graf może zostać zoptymalizowany podczas przygotowania sesji, natomiast model lub wybrane operacje są przekształcane do INT8. Na nowoczesnych procesorach x86 lub ARM szczególne znaczenie mają instrukcje wektorowe i mechanizmy przyspieszające operacje na liczbach całkowitych, takie jak AVX-512 czy VNNI, o ile są dostępne i wykorzystywane przez konkretną konfigurację. [2]
CPU jest atrakcyjny tam, gdzie ruch jest umiarkowany, żądania są nieregularne, a utrzymywanie GPU przez całą dobę byłoby kosztowne. Instancje ogólnego przeznaczenia są zwykle łatwiejsze do autoskalowania, a aplikacja nie wymaga sterowników GPU, zgodności CUDA ani rezerwowania drogich akceleratorów. Dla modeli klasyfikacyjnych, detekcyjnych i wielu modeli tabularnych różnica jakościowa może być niewielka, o ile kwantyzację przeprowadzono z reprezentatywnym zbiorem kalibracyjnym.
Ryzyko dotyczy przede wszystkim dokładności. INT8 nie jest neutralnym przełączeniem typu danych. Wrażliwe mogą być warstwy z dużym zakresem wartości, nietypowe rozkłady aktywacji, modele z małą tolerancją błędu oraz zadania, w których niewielka zmiana wyniku zmienia decyzję biznesową. Konieczne są testy metryk modelu, a nie tylko porównanie kilku przykładowych predykcji.
- Plusy: niższy koszt infrastruktury, prostsze wdrożenie i autoscaling, brak zależności od GPU;
- Minusy: możliwy spadek jakości po INT8, mniejsza przepustowość dla bardzo dużych modeli, zależność od właściwego doboru liczby wątków;
- Dla kogo: dla modeli małych i średnich, usług o zmiennym ruchu oraz systemów, w których koszt stałej obecności GPU jest nieuzasadniony.
Wariant C: ONNX Runtime na GPU z CUDA Execution Provider
CUDA Execution Provider pozwala wykonywać operacje ONNX Runtime na GPU NVIDIA. To naturalny wariant dla modeli, które korzystają z równoległości obliczeń, mają duże macierze lub muszą obsługiwać wiele żądań z niską latencją. W porównaniu z bezpośrednim servingiem frameworka możliwe jest zmniejszenie narzutu oraz lepsze wykorzystanie zoptymalizowanego grafu, ale rezultat zależy od pokrycia operatorów i sposobu podziału pracy.
Warto przeczytać również powiązany artykuł: Analiza ruchu klientów w sklepie stacjonarnym z AI.
Nie każdy węzeł musi zostać wykonany na GPU. Jeśli część grafu trafi na CPU, dane mogą być kopiowane między urządzeniami. Przy małych tensorach koszt transferu i synchronizacji może przewyższyć zysk z akceleracji. W konfiguracji produkcyjnej trzeba sprawdzić, czy podział grafu nie tworzy wielu krótkich odcinków CPU–GPU oraz czy wszystkie operacje krytyczne dla latencji są obsługiwane przez wybrany provider.
Wariant C ma sens, gdy GPU jest już dostępne, model jest wystarczająco duży lub ruch wystarczająco intensywny, a wymagania czasowe nie są osiągalne na CPU. Trzeba jednak uwzględnić koszt obrazu kontenera, sterowników, wersji CUDA, alokacji pamięci i obserwowalności. Sama informacja, że proces widzi kartę, nie potwierdza prawidłowego wykorzystania akceleratora.
- Plusy: wysoka przepustowość dla odpowiednich modeli, możliwość obsługi niskiej latencji, wykorzystanie istniejącej infrastruktury NVIDIA;
- Minusy: koszt GPU, większa złożoność zależności, transfery CPU–GPU i ryzyko niepełnego pokrycia operatorów;
- Dla kogo: dla dużego ruchu, modeli intensywnie macierzowych i usług, w których czas odpowiedzi uzasadnia koszt akceleratora.
Wariant D: ONNX Runtime z TensorRT Execution Provider
TensorRT Execution Provider kieruje wybrane fragmenty grafu do TensorRT, który może budować zoptymalizowany silnik dla konkretnego modelu i urządzenia NVIDIA. Ten wariant często jest rozważany przy wymagających modelach wizji komputerowej, transformatorach i innych obciążeniach, gdzie maksymalna przepustowość GPU ma bezpośrednie znaczenie dla kosztu jednostkowego.
Korzyścią jest możliwość zastosowania agresywniejszych optymalizacji sprzętowych, fuzji oraz precyzji FP16 lub INT8, jeśli model i proces walidacji na to pozwalają. Ceną jest większa zależność od wersji sprzętu, sterowników, TensorRT i profili kształtów wejściowych. Silnik zbudowany dla określonej konfiguracji może nie być łatwo przenośny na inną generację GPU.
- Plusy: możliwość maksymalnego wykorzystania GPU, fuzja operacji, obsługa FP16 i INT8 oraz wysoka przepustowość dla kompatybilnych grafów;
- Minusy: największa złożoność wdrożenia, zależność od konkretnego sprzętu i wersji stosu NVIDIA, dłuższy proces budowania oraz testowania silników;
- Dla kogo: dla zespołów obsługujących duży i stabilny ruch, wymagających niskiej latencji lub wysokiej przepustowości oraz gotowych utrzymywać osobny proces optymalizacji i walidacji silnika.
W praktyce TensorRT wymaga również zarządzania cyklem życia silnika. Zmiana modelu, wersji sterownika, biblioteki albo profilu kształtów może wymagać ponownego zbudowania artefaktu i wykonania testów regresyjnych. Warto przechowywać silniki razem z informacją o wersji modelu, typie GPU, użytej precyzji oraz zakresie obsługiwanych wymiarów dynamicznych.
Ten wariant jest mniej odpowiedni dla modeli często zmienianych, eksperymentalnych lub wdrażanych na wielu, znacząco różniących się konfiguracjach sprzętowych. Koszt utrzymania ścieżki TensorRT może wtedy przewyższyć zysk z dodatkowej optymalizacji. Przed wyborem trzeba sprawdzić nie tylko najlepszy wynik benchmarku, lecz także czas budowania silnika, zachowanie po zmianie batch size i stabilność p95 oraz p99.
Jak wybrać wariant do konkretnego obciążenia
Porównanie wariantów powinno zaczynać się od ograniczeń usługi, a nie od dostępnego narzędzia. Ten sam model może wymagać innej architektury w systemie z kilkoma żądaniami na sekundę, innej w interaktywnym API, a jeszcze innej w przetwarzaniu wsadowym. Znaczenie mają również wymagania dotyczące dokładności, częstotliwość aktualizacji modelu i liczba środowisk, które trzeba utrzymywać.
| Wariant | Latencja i przepustowość | Koszt operacyjny | Zgodność i przenośność | Najlepsze zastosowanie |
|---|---|---|---|---|
| A: framework | Przewidywalne uruchomienie, ale potencjalnie większy narzut wykonania | Prosty start, lecz wyższe zużycie zasobów może zwiększać koszt jednostkowy | Najwyższa zgodność z kodem treningowym | Prototypy produkcyjne, niestandardowe operatory, szybkie wdrożenia |
| B: ONNX Runtime CPU INT8 | Dobra efektywność dla małych i średnich modeli oraz umiarkowanego ruchu | Zwykle korzystny przy zmiennym ruchu i braku stałego GPU | Dobra przenośność, ale konieczna walidacja kwantyzacji | API o nieregularnym ruchu, modele klasyfikacyjne, detekcyjne i tabularne |
| C: ONNX Runtime CUDA | Wysoka przepustowość dla grafów dobrze wykorzystujących GPU | Opłacalny przy odpowiednim obciążeniu akceleratora | Średnia; zależność od CUDA, sterowników i pokrycia operatorów | Duże modele, intensywne obliczenia macierzowe, wymagająca latencja |
| D: ONNX Runtime TensorRT | Największy potencjał optymalizacji na zgodnym GPU | Może obniżyć koszt żądania przy dużej przepustowości, ale wymaga inwestycji w utrzymanie | Najmniejsza przenośność między konfiguracjami sprzętowymi | Stabilne obciążenie, produkcja na określonej rodzinie GPU, rygorystyczne SLA |
Kryterium pierwsze: wymagany czas odpowiedzi
Jeśli usługa ma łagodne wymagania czasowe, nie ma powodu zaczynać od najbardziej złożonego wariantu. ONNX Runtime na CPU może zapewnić wystarczającą wydajność przy prostszym wdrożeniu i łatwiejszym skalowaniu. Warto najpierw sprawdzić, czy model po optymalizacji grafu i ewentualnej kwantyzacji spełnia wymagania dla p95 i p99. [3]
Gdy limit latencji jest niski, a model wykonuje dużo operacji równoległych, należy porównać wariant C i D na rzeczywistym sprzęcie. TensorRT ma większy sens wtedy, gdy różnica względem CUDA jest istotna także po uwzględnieniu kolejek, transferów, preprocessing oraz serializacji odpowiedzi. Jeżeli poprawa dotyczy wyłącznie krótkiego fragmentu grafu, dodatkowa złożoność może nie przynieść wymiernej korzyści.
Kryterium drugie: charakter ruchu
Dla ruchu stałego i wysokiego łatwiej uzasadnić koszt przygotowania środowiska GPU oraz strojenia batchingu. W takim przypadku większa przepustowość może obniżyć koszt pojedynczego żądania, ponieważ instancja jest lepiej wykorzystana. Przy ruchu skokowym lub trudnym do przewidzenia korzystniejszy bywa CPU, który można szybciej skalować i który nie pozostaje drogim, bezczynnym zasobem między seriami żądań.
W usługach interaktywnych trzeba uważać na automatyczne zwiększanie batcha. Łączenie żądań może poprawić przepustowość, ale jednocześnie zwiększyć czas oczekiwania pierwszego elementu w kolejce. Konfigurację należy oceniać na reprezentatywnym rozkładzie ruchu, a nie tylko na maksymalnym obciążeniu testowym.
Kryterium trzecie: stabilność modelu i procesu wdrożeniowego
Model często zmieniany, eksportowany z niestandardowymi operatorami albo uruchamiany na wielu typach maszyn może lepiej pasować do wariantu A lub B. Mniejsza liczba artefaktów i zależności upraszcza rollback, testy regresyjne oraz obsługę kilku wersji modelu równocześnie.
Wariant D jest uzasadniony, gdy graf i sprzęt są względnie stabilne, a zespół może zautomatyzować budowanie silnika, testy dokładności i benchmarki. Pipeline wdrożeniowy powinien wykrywać nieobsługiwane operatory, porównywać wyniki z referencyjnym wykonaniem oraz blokować publikację silnika, jeśli pogarszają się metryki jakości albo ogony rozkładu latencji.
Rekomendowany sposób wyboru
Najbezpieczniej potraktować wariant A jako punkt odniesienia funkcjonalnego, a nie automatycznie jako rozwiązanie docelowe. Następnie warto zbudować porównywalne ścieżki dla ONNX Runtime na CPU i, jeśli uzasadnia to obciążenie, dla CUDA lub TensorRT. Każdy wariant powinien korzystać z tego samego zestawu danych, tych samych limitów czasu, podobnej polityki kolejkowania i identycznego zakresu pomiarów.
- Zacznij od zgodności: potwierdź, że model eksportuje się poprawnie, a wyniki mieszczą się w zaakceptowanej tolerancji względem implementacji referencyjnej.
- Sprawdź wariant CPU: zmierz wykonanie FP32 oraz INT8, dobierając liczbę wątków do konkretnej instancji i obserwując wpływ na p95, p99 oraz współdzielenie zasobów.
- Dodaj GPU tylko przy uzasadnieniu: porównaj pełny czas żądania, a nie sam czas kerneli. Uwzględnij transfery, batching, zajętość pamięci i koszt instancji.
- Rozważ TensorRT przy stabilnym obciążeniu: wybierz go wtedy, gdy zysk z optymalizacji utrzymuje się w warunkach produkcyjnych i rekompensuje koszt budowania oraz utrzymania silników.
- Wybierz na podstawie kosztu jednostkowego: zestaw koszt całej infrastruktury z liczbą poprawnie obsłużonych żądań przy wymaganej jakości i poziomie p99.
W typowym procesie decyzyjnym wariant B jest rozsądnym pierwszym celem dla zespołów, które chcą ograniczyć koszty bez wprowadzania GPU. Wariant C warto wybrać, gdy wymagania czasowe lub rozmiar modelu uzasadniają akcelerację, ale pełna zależność od TensorRT byłaby przedwczesna. Wariant D najlepiej sprawdza się jako rozwiązanie docelowe dla dojrzałych usług o dużym, stabilnym obciążeniu i jasno określonej platformie sprzętowej.
Jeżeli model nie mieści się w ograniczeniach ONNX Runtime albo używa operatorów krytycznych dla jakości, wariant A pozostaje uzasadniony. Nie jest porażką optymalizacji, lecz świadomym wyborem zgodności. Dopiero zestawienie tej zgodności z pomiarem kosztu, latencji i nakładu utrzymania pozwala ocenić, czy migracja do innego wariantu rzeczywiście poprawi działanie usługi.
Kluczowe Wnioski
- Sama konwersja modelu do formatu ONNX nie gwarantuje krótszej latencji ani niższego kosztu.
- Przed wyborem runtime’u trzeba mierzyć p50, p95 i p99 oraz rozdzielić preprocessing, kolejkę, transfer danych, inferencję i postprocessing.
- ONNX Runtime na CPU z kwantyzacją INT8 może ograniczyć koszty przy umiarkowanym lub nieregularnym ruchu, ale wymaga kontroli jakości predykcji.
- ONNX Runtime na GPU ma sens wtedy, gdy model i obciążenie pozwalają efektywnie wykorzystać akcelerator, a podział pracy CPU–GPU nie powoduje nadmiernych transferów.
- Skalowanie liczbą instancji nie zastępuje profilowania grafu, ustawienia batchingu ani optymalizacji ścieżki danych.
- Najlepszy wariant należy wybrać na podstawie wymaganej latencji, jakości modelu i kosztu obsługi pojedynczego żądania.






