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.
