TL;DR: Прогнал ResNet-50 через PyTorch, ONNX Runtime, OpenVINO, TensorRT и TVM в FP32/FP16/INT8/INT4 на CPU Ryzen 9 6900HS и GPU RTX 3070 Ti Laptop в 46 конфигурациях. Лучший CPU: ONNX Runtime static INT8 — 15.4 ms / 64.8 img/s (×4.0 от torch baseline при bs=1). Лучший GPU: TensorRT INT8 — 1.16 ms / 863 img/s (×5.8). torch.compile + FP16 даёт ×2.6 ускорения без смены движка. Код и данные: github.com/DmitriyValetov/resnet50-inference-benchmark.
Введение
Есть такая рубрика — бенчмарки гонять. Эта статья как раз из таких. По мотивам ряда публикаций про inference-движки захотелось сделать свою — с более подробными метриками и открытым кодом.
Inference-движки нам нужны, когда модель обучена и её нужно гонять в проде, либо масштабно валидировать. В следующей таблице приведу многообразие движков и зону их применимости:
Фреймворк |
Сервер/Облако |
Edge |
Примечание |
|---|---|---|---|
TensorRT |
✅ |
✅ |
Только NVIDIA GPU |
ONNX Runtime |
✅ |
✅ |
Универсальный вариант |
OpenVINO |
✅ |
✅ |
В основном заточен под intel экосистему, но на rysen тоже показал прирост производительности |
TVM |
✅ |
✅ |
Компилятор, µTVM для MCU edge-ai-vision |
ExecuTorch |
❌ |
✅ |
Замена PyTorch Mobile |
TFLite/LiteRT |
❌ |
✅ |
Движок под Android/iOS/RPi |
NCNN |
❌ |
✅ |
Tencent edge engine |
MNN |
❌ |
✅ |
Alibaba edge engine |
Paddle Lite |
❌ |
✅ |
Baidu edge engine |
Так же существуют различные методы оптимизации производительности (в настоящей статье речь пойдет только о тех, которые не меняют архитектуру сети), поддерживаемые большей частью движков, пусть и с местными нюансами, это: снижение точности вычислений (FP32→FP16/INT8/INT4), kernel fusion, графовые оптимизации.
В этой статье испытаниям подвергнется ResNet-50 в 46 конфигурациях на 5 движках, с цифрами, таблицами и графиками.
Испытательный стенд
Компонент |
Конфигурация |
|---|---|
CPU |
AMD Ryzen 9 6900HS (8C/16T, Zen 3+) |
GPU |
NVIDIA RTX 3070 Ti Laptop (8 GB GDDR6) |
RAM |
32 GB DDR5 |
Окружение |
Docker, Ubuntu 22.04, CUDA 12.8 |
Модель |
ResNet-50, веса ImageNet1K_V1 |
Датасет для эвала |
10k изображений (подмножество ImageNet val) - Top-1 / Top-5 Accuracy |
Как считаем скорость |
Latency/Throughput при bs=1 и bs=64 |
Warmup |
10 итераций |
Measurement |
50–100 итераций, медиана |
Сетка batch size |
1, 8, 16, 32, 64 |
Baseline |
torch FP32 eager mode |
Методы
Всего прогнано 46 конфигураций по пяти движкам:
Движок |
Конфигураций |
Что внутри |
|---|---|---|
PyTorch |
19 |
FP32/FP16 CPU+CUDA, autocast, 6 × torch.compile, 3 quant (dynamic/static FX/PT2E), PT2E+compile ×2, W4 GPU (eager + 2 compile) |
ONNX Runtime |
8 |
FP32/FP16/INT8 dynamic/INT8 static × (CPU + CUDA) |
OpenVINO |
3 |
FP32/FP16/INT8 — CPU |
TensorRT |
3 |
FP32/FP16/INT8 — CUDA |
TVM |
13 |
FP32/FP16/INT8 × (CPU + CUDA), default/MetaSchedule/AutoTVM |
Всего |
46 |
Torch: бейзлайн и богатый набор оптимизаций
Torch — то, с чего все начинают: обучение, эксперименты. Чаще всего на нем инференс в проде не гоняют, но, к моему удивлению, внутри экосистемы PyTorch есть большой спектр настройки инференса.
Сетапы torch
Precision |
Метод |
Устройства |
Примечание |
|---|---|---|---|
FP32 |
eager |
CPU, CUDA |
baseline |
FP16 |
|
CPU |
вход float16 |
FP16 |
|
CUDA |
веса FP32, где FP16 — выбирает PyTorch |
FP32 |
torch.compile (reduce-overhead/max-autotune) |
CPU |
Inductor |
FP16 |
torch.compile (reduce-overhead/max-autotune) |
CPU, CUDA |
Inductor |
INT8 |
torchao weight-only |
CPU |
dynamic, только веса |
INT8 |
FX static PTQ (prepare → калибровка → convert) |
CPU |
веса + активации |
INT8 |
PT2E (torch.export + x86-квантайзер) |
CPU |
+ compile опционально |
INT4 W4 |
torchao, Linear → INT4, модель BF16 |
CUDA |
weight-only, eager + compile |
Таблица результатов
Конфигурация |
Device |
Lat/img bs=1 (ms) |
Thr bs=1 (img/s) |
Lat/img bs=64 (ms) |
Thr bs=64 (img/s) |
Top-1 (%) |
|---|---|---|---|---|---|---|
FP32 (cpu baseline) |
CPU |
61.6 |
16.2 |
119.6 |
8.4 |
76.15 |
FP32 + compile(reduce) |
CPU |
77.2 |
13.0 |
56.2 |
17.8 |
76.15 |
FP16 (half) |
CPU |
2250.1 |
0.4 |
1230.0 |
0.8 |
76.15 |
INT8 dynamic |
CPU |
150.7 |
6.6 |
120.5 |
8.3 |
76.09 |
INT8 static FX |
CPU |
26.7 |
37.5 |
25.9 |
38.6 |
75.89 |
INT8 PT2E |
CPU |
158.1 |
6.3 |
260.4 |
3.8 |
76.04 |
PT2E + compile(max) |
CPU |
66.0 |
14.5 |
59.6 |
16.8 |
76.04 |
FP32 (gpu baseline) |
CUDA |
6.68 |
149.8 |
1.55 |
646.4 |
76.15 |
FP16 autocast |
CUDA |
9.4 |
105.9 |
0.94 |
1094.0 |
76.15 |
FP16 + compile(reduce) |
CUDA |
2.57 |
389.2 |
0.69 |
1454.1 |
76.15 |
FP16 + compile(max) |
CUDA |
3.69 |
345.2 |
0.71 |
1412.8 |
76.15 |
INT4 W4 eager |
CUDA |
8.09 |
123.6 |
0.98 |
1019.3 |
76.08 |
INT4 W4 + compile(reduce) |
CUDA |
3.30 |
303.2 |
0.78 |
1290.9 |
76.03 |
Lat/img = задержка на одно изображение (per-batch latency ÷ batch size). Thr = пропускная способность (img/s). Top-1 (%) = доля изображений, для которых модель правильно предсказала класс.
Победители внутри Torch
Конфигурация |
Device |
bs=1 Lat/img |
bs=1 Thr |
bs=64 Lat/img |
bs=64 Thr |
Top-1 |
|---|---|---|---|---|---|---|
INT8 static FX |
CPU |
26.7 ms (×2.3) |
37.5 img/s (×2.3) |
25.9 ms (×4.6) |
38.6 img/s (×4.6) |
75.89 |
FP16 + compile(reduce) |
GPU |
2.57 ms (×2.6) |
389 img/s (×2.6) |
0.69 ms (×2.2) |
1454 img/s (×2.2) |
76.15 |
×-ы относительно torch FP32 eager на соответствующем устройстве


ONNX Runtime
Предварительно модель экспортируется из torch в onnx (opset 17), а потом производится инференс через ONNX Runtime. FP32 и FP16 графы напрямую экспортируются из torch, а квантованные INT8 строятся уже из FP32-ONNX средствами onnxruntime.quantization.
Сетапы onnxruntime
Precision |
Quant |
Устройства |
|---|---|---|
FP32 |
— |
CPU, CUDA |
FP16 |
— |
CPU, CUDA |
INT8 |
dynamic (weight-only) |
CPU, CUDA |
INT8 |
static (QDQ) |
CPU, CUDA |
Таблица результатов
Конфигурация |
Device |
Lat/img bs=1 (ms) |
Thr bs=1 (img/s) |
Lat/img bs=64 (ms) |
Thr bs=64 (img/s) |
Top-1 (%) |
|---|---|---|---|---|---|---|
FP32 |
CPU |
27.9 |
35.9 |
25.9 |
38.6 |
76.15 |
FP16 |
CPU |
64.8 |
15.4 |
34.3 |
29.1 |
76.14 |
Dynamic INT8 |
CPU |
26.5 |
37.8 |
38.5 |
25.9 |
75.54 |
Static INT8 (QDQ) |
CPU |
15.4 |
64.8 |
12.0 |
83.5 |
73.78 |
FP32 |
CUDA |
5.27 |
189.8 |
1.69 |
591.9 |
76.15 |
FP16 |
CUDA |
5.65 |
177.1 |
1.07 |
934.1 |
76.14 |
Static INT8 |
CUDA |
9.25 |
108.2 |
2.27 |
440.9 |
75.73 |
Dynamic INT8 |
CUDA |
46.7 |
21.4 |
38.0 |
26.3 |
75.54 |
Победители внутри ONNX Runtime (все множители — к FP32 eager):
Конфигурация |
Device |
bs=1 Lat/img |
bs=1 Thr |
bs=64 Lat/img |
bs=64 Thr |
Top-1 |
|---|---|---|---|---|---|---|
Static INT8 (QDQ) |
CPU |
15.4 ms (×4.0) |
64.8 img/s (×4.0) |
12.0 ms (×10.0) |
83.5 img/s (×10.0) |
73.78 |
FP16 |
GPU |
5.65 ms (×1.2) |
177 img/s (×1.2) |
1.07 ms (×1.4) |
934 img/s (×1.4) |
76.14 |


OpenVINO
Да, этот фреймворк заточен под работу с железом intel, но я таки попробую его на rysen процессоре. Модель также изначально экспортируется из torch в onnx (opset 17) и конвертируется в OpenVINO IR (.xml/.bin) через openvino.convert_model. FP16 — сжатием весов при сохранении IR (compress_to_fp16=True в openvino.convert_model), INT8 — статический PTQ с калибровкой (FakeQuantize в графе, INT8 веса и активации, калибровка на тех же картинках, что для torch/ORT).
Сетапы openvino
Precision |
Quant |
Устройства |
|---|---|---|
FP32 |
— |
CPU |
FP16 |
— |
CPU |
INT8 |
static PTQ |
CPU |
Таблица результатов
Конфигурация |
Lat/img bs=1 (ms) |
Thr bs=1 (img/s) |
Lat/img bs=64 (ms) |
Thr bs=64 (img/s) |
Top-1 (%) |
|---|---|---|---|---|---|
FP32 |
39.9 |
25.1 |
38.9 |
25.7 |
76.15 |
FP16 |
38.4 |
26.1 |
36.9 |
27.1 |
76.15 |
INT8 |
18.1 |
55.3 |
18.4 |
54.4 |
73.46 |
Победитель внутри OpenVINO
Конфигурация |
Device |
bs=1 Lat/img |
bs=1 Thr |
bs=64 Lat/img |
bs=64 Thr |
Top-1 |
|---|---|---|---|---|---|---|
INT8 |
CPU |
18.1 ms (×3.4) |
55.3 img/s (×3.4) |
18.4 ms (×6.5) |
54.4 img/s (×6.5) |
73.46 |
Выводы: При bs=1 INT8 втрое быстрее baseline (18.1 ms, ×3.4). При bs=64 — ×6.5 быстрее baseline, но per-image latency почти не падает с ростом batch (с 18.1 до 18.4 ms). Для сравнения, ORT static INT8 снижает per-image latency с 15.4 до 12.0 ms. Пусть OV и слабо масштабируется на батчи относительно ORT,результат - достойный.

TVM
Я был наслышан, что tvm тонко настраивается под железо, но лично мне совсем не удалось выжать из него эффективность. Тут аналогично: torch в onnx, затем грузим в Relay и компилируем под llvm (CPU) или cuda (GPU). Граф фиксирован по batch — для каждого bs отдельная компиляция и тюнинг.
Сетапы TVM
Precision |
Schedule |
Устройства |
|---|---|---|
FP32 |
default, MetaSchedule, AutoTVM |
CPU, CUDA |
FP16 |
default, AutoTVM |
CPU, CUDA |
INT8 |
default |
CPU, CUDA |
Таблица результатов
Конфигурация |
Device |
Lat/img bs=1 (ms) |
Thr bs=1 (img/s) |
Lat/img bs=64 (ms) |
Thr bs=64 (img/s) |
Top-1 (%) |
|---|---|---|---|---|---|---|
FP32 default |
CPU |
93.2 |
10.7 |
3.5 |
9.7 |
66.7 |
FP32 MetaSchedule |
CPU |
177.4 |
5.6 |
3.5 |
4.4 |
66.7 |
FP32 AutoTVM |
CPU |
196.1 |
5.1 |
3.2 |
4.9 |
66.7 |
FP16 default |
CPU |
25146.2 |
<0.1 |
403.9 |
<0.1 |
— |
INT8 default |
CPU |
638.8 |
1.6 |
9.7 |
1.6 |
— |
FP32 default |
CUDA |
6.86 |
145.8 |
0.05 |
285.2 |
66.7 |
FP16 default |
CUDA |
15.8 |
63.4 |
0.04 |
376.4 |
66.7 |
INT8 default |
CUDA |
206.1 |
4.8 |
4.7 |
3.3 |
— |
Accuracy на подмножестве из 100 семплов (~76% на полном ImageNet). FP16 CPU и INT8 — eval не запускался из-за ужасной ожидаемой длительности.
Победитель внутри TVM (× от torch FP32 eager):
Конфигурация |
Device |
bs=1 Lat/img |
bs=1 Thr |
bs=64 Lat/img |
bs=64 Thr |
Top-1 |
|---|---|---|---|---|---|---|
FP32 default |
CPU |
93.2 ms (×0.7) |
10.7 img/s (×0.7) |
3.5 ms (×1.2) |
9.7 img/s (×1.2) |
66.7 |
FP32 default |
CUDA |
6.86 ms (×1.0) |
145.8 img/s (×1.0) |
0.05 ms (×0.4) |
285.2 img/s (×0.4) |
66.7 |
Выводы: Лучшие сетапы по эффективности порядка eagere mode torch.


TensorRT
Все также экспортируем модель в onnx, onnx парсим в TensorRT, который пересобирает граф заново под конкретную GPU — с kernel fusion, автовыбором оптимальных ядер (tactic sources extended, opt level 5) и фиксированным optimization profile (min=1, max=64). На выходе — самодостаточный engine (.engine), не требующий ни PyTorch, ни ONNX Runtime. INT8 — с entropy-калибровкой на тех же картинках и FP16-fallback для слоёв, не влезающих в INT8.
Сетапы TensorRT
Precision |
Features |
Устройства |
|---|---|---|
FP32 |
— |
CUDA |
FP16 |
BuilderFlag.FP16 |
CUDA |
INT8 |
entropy calibrator + FP16 fallback |
CUDA |
Таблица результатов
Конфигурация |
Lat/img bs=1 (ms) |
Thr bs=1 (img/s) |
Lat/img bs=64 (ms) |
Thr bs=64 (img/s) |
Top-1 (%) |
|---|---|---|---|---|---|
FP32 |
4.90 |
204.2 |
1.60 |
625.3 |
76.16 |
FP16 |
1.48 |
675.6 |
0.56 |
1788.5 |
76.14 |
INT8 + FP16 fallback |
1.16 |
863.3 |
0.19 |
5331.0 |
76.10 |
Победитель внутри TensorRT (× от torch FP32 eager GPU):
Конфигурация |
Device |
bs=1 Lat/img |
bs=1 Thr |
bs=64 Lat/img |
bs=64 Thr |
Top-1 |
|---|---|---|---|---|---|---|
INT8 + FP16 fallback |
CUDA |
1.16 ms (×5.8) |
863 img/s (×5.8) |
0.19 ms (×8.2) |
5331 img/s (×8.2) |
76.10 |
Выводы: INT8 — ×5.8 по latency и throughput при bs=1, ×8.2 при bs=64. Абсолютный рекорд среди всех движков.

Итог по CPU
Движок |
Конфигурация |
Lat/img bs=1 (ms) |
Thr bs=1 (img/s) |
Lat/img bs=64 (ms) |
Thr bs=64 (img/s) |
Top-1 (%) |
|---|---|---|---|---|---|---|
ORT |
INT8 static QDQ |
15.4 |
64.8 |
12.0 |
83.5 |
73.78 |
OV |
INT8 |
18.1 |
55.3 |
18.4 |
54.4 |
73.46 |
Torch |
INT8 static FX |
26.7 |
37.5 |
25.9 |
38.6 |
75.89 |
TVM |
FP32 default |
93.2 |
10.7 |
3.5 |
9.7 |
66.7 |
Итог по GPU
Движок |
Конфигурация |
Lat/img bs=1 (ms) |
Thr bs=1 (img/s) |
Lat/img bs=64 (ms) |
Thr bs=64 (img/s) |
Top-1 (%) |
|---|---|---|---|---|---|---|
TRT |
INT8 |
1.16 |
863.3 |
0.19 |
5331.0 |
76.10 |
Torch |
FP16 + compile(reduce) |
2.57 |
389.2 |
0.69 |
1454.1 |
76.15 |
ORT |
FP16 |
5.65 |
177.1 |
1.07 |
934.1 |
76.14 |
TVM |
FP32 default |
6.86 |
145.8 |
0.05 |
285.2 |
66.7 |
На этом всё!
Код, JSON-результаты и Docker-конфигурации — github.com/DmitriyValetov/resnet50-inference-benchmark.