5 движков, 1 resnet: битва inference-рантаймов 2026

от автора

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

model.half()

CPU

вход float16

FP16

torch.amp.autocast

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 на соответствующем устройстве

Latency и Throughput vs batch size — все конфигурации PyTorch на CPU.

Latency и Throughput vs batch size — все конфигурации PyTorch на CPU.
Latency и Throughput vs batch size — все конфигурации PyTorch на CUDA.

Latency и Throughput vs batch size — все конфигурации PyTorch на CUDA.

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

Latency и Throughput vs batch size — все конфигурации ONNX Runtime на CPU.

Latency и Throughput vs batch size — все конфигурации ONNX Runtime на CPU.
Latency и Throughput vs batch size — все конфигурации ONNX Runtime на CUDA.

Latency и Throughput vs batch size — все конфигурации ONNX Runtime на CUDA.

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,результат — достойный.

Latency и Throughput vs batch size — все конфигурации OpenVINO на CPU.

Latency и Throughput vs batch size — все конфигурации OpenVINO на CPU.

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.

Latency и Throughput vs batch size — все конфигурации TVM на CPU.

Latency и Throughput vs batch size — все конфигурации TVM на CPU.
Latency и Throughput vs batch size — все конфигурации TVM на CUDA.

Latency и Throughput vs batch size — все конфигурации TVM на CUDA.

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. Абсолютный рекорд среди всех движков.

Latency и Throughput vs batch size — все конфигурации TensorRT на CUDA.

Latency и Throughput vs batch size — все конфигурации TensorRT на CUDA.

Итог по 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.

ссылка на оригинал статьи https://habr.com/ru/articles/1053650/