От FastAPI predict к Triton:
гид по оптимизации
ML-инференса
в продакшене

FastAPI + model.predict - минусы MVP подхода
На этапе разработки прототипа (MVP) или проведения экспериментов самым быстрым путем доставки ML-модели в продакшен является использование стандартных веб-фреймворков, таких как FastAPI или Flask: обученная модель загружается непосредственно в память процесса, а инференс выполняется средствами того же фреймворка, на котором она обучалась (например, вызывается напрямую через PyTorch).

Код в таком случае выглядит примерно так:
@app.post("/predict")
def predict(request):
    data = preprocess(request)
    prediction = model(data) 
    return {"result": prediction.tolist()}
Причина в том, что обучение и инференс
ML-моделей — это фундаментально разные инженерные задачи, требующие разных инструментов.

При увеличении нагрузки столкновение с ограничениями такого подхода неизбежно: задержка ответа сервера может резко возрастать, GPU используется неравномерно, память быстро заполняется, в некоторых случаях процесс падает с ошибкой CUDA Out Of Memory.

Во время обучения нам важна гибкость. Фреймворки вроде PyTorch или TensorFlow (v2) созданы, чтобы хранить градиенты, поддерживать динамические графы вычислений и позволять исследователю менять архитектуру на лету. Это удобно для НИОКР (R&D), но так же являются большими накладными расходами (оверхедом) для продакшена.
В продакшене ключевым приоритетом становятся стабильность и скорость. Веса модели заморожены, градиенты не нужны, а граф вычислений статичен. Запуск "сырой" модели через стандартный Python-интерпретатор тянет за собой ряд проблем:
  • Python Overhead и GIL

    Global Interpreter Lock (GIL) не позволяет эффективно распараллеливать обработку запросов на уровне python-кода (обработка запросов, группировка данных (батчинг), препроцессинг), создавая “бутылочное горлышко” еще до того, как данные попадут на видеокарту. Это снижает пропускную способность и увеличивает время отклика.
  • Отсутствие аппаратной оптимизации

    PyTorch по умолчанию выполняет операции последовательно без дополнительной оптимизации, и не адаптирует модель под конкретные графические процессоры (например, NVIDIA A100 или T4).
  • Неэффективная работа с памятью

    Из-за отсутствия глобальной оптимизации программный каркас (фреймворк) выполняет операции последовательно и не "видит" весь вычислительный граф целиком. Это приводит к избыточным временным распределениям (аллокациям) и неэффективному использованию буферов GPU.
Сегодня наивный подход все чаще становится "бутылочным горлышком" для бизнеса. С ростом объемов обрабатываемых данных и переходом ИИ из статуса “эксперимента” в полноценный сервис, компании сталкиваются с суровой реальностью: инференс стоит дорого. По данным на начало 2026 года, затраты на исполнение моделей в облаках впервые превысили затраты на их обучение. В условиях, когда аренда мощных GPU (вроде NVIDIA H100 или грядущих архитектур) обходится в значительные суммы, неэффективный код в продакшене - это прямые убытки.
Бизнес-сегмент сегодня активно использует ML в трех ключевых направлениях:
  • Классической Компьютерное зрение (Computer Vision) и НЛП (NLP)

    Системы безопасности, анализ документов, дефектоскопия. Здесь критически важна пропускная способность.
  • Прогнозирование и рекомендательные системы

    Работа с огромными объемами табличных данных, где требуется минимальная задержка.
  • Периферийные вычисления (Edge AI)

    Это один из главных трендов 2026 года. Компании стремятся перенести ИИ вычисления из дорогих облаков непосредственно "на места" - в умные камеры, датчики на заводах или устройства пользователей. Такой подход снижает затраты на трафик, решает вопросы приватности и позволяет системе работать даже при отсутствии связи с интернетом.
В этой статье мы разберем, как перестать запускать модели «в лоб» и начать использовать специализированные движки инференса. Мы пройдем путь от универсального ONNX Runtime до TensorRT и серверных решений уровня NVIDIA Triton. Наша цель — максимизировать пропускную способность и минимизировать задержки, не потеряв при этом в точности.
Важное уточнение: Специализированные фреймворки для инференса больших языковых моделей (vLLM, TGI, llama.cpp) мы разберём в следующей статье. Их специфика — KV-кэш и авторегрессионная генерация — заслуживает отдельного глубокого анализа.

Анатомия оптимизации:
за счет чего происходит ускорение

Прежде чем сравнивать инструменты, давайте разберемся в физике процесса. Магия ускорения инференса не берется из воздуха - это результат жесткой математической и аппаратной оптимизации. Большинство движков используют комбинацию четырех основных техник.

Оптимизация графа вычислений и слияние слоев

Нейросеть — это направленный граф вычислений. В "сыром" виде он часто избыточен.
Представьте классический блок сверточной сети: Conv2d -> Bias -> ReLU.
В стандартном исполнении это три отдельных запуска GPU-кернелов (микропрограмм для видеокарты).
  1. Загрузить данные из видеопамяти (VRAM) в кэш чипа. Вычислить свертку. Записать результат обратно в VRAM.
  2. Загрузить результат. Добавить смещение (Bias). Записать обратно.
  3. Загрузить результат. Применить функцию активации. Записать обратно.
Самая дорогая операция здесь — это не сложение чисел, а хождение в память (memory access). Движки инференса анализируют граф и делают Operator Fusion. Они компилируют эти три операции в один супер-кернел. Данные загружаются один раз, обрабатываются сразу всеми операциями и выгружаются.

Результат: снижение задержек и уменьшение нагрузки на шину памяти.

Квантизация

По умолчанию модели обучаются в формате FP32 (32-битные числа с плавающей точкой). Это дает высокую точность, но каждое число занимает 4 байта.
Однако для инференса такая точность часто избыточна. Нейросети удивительно устойчивы к шуму.
Мы можем перевести веса модели в:

  • FP16 / BF16 (Half Precision): 2 байта на число. На современных GPU с тензорными ядрами достигается ускорение почти в 2 раза, потребление памяти в 2 раза меньше. Потери качества для большинства моделей минимальны или вовсе отсутствуют.
  • INT8 (Целые числа): 1 байт на число. Экстремальное ускорение и экономия памяти в 4 раза. Требует калибровки (calibration), чтобы не потерять точность, но позволяет запускать очень увесистые модели на дешевом железе.

Более экстремальные варианты квантования (INT4 и ниже) здесь не рассматриваются, но, несмотря на то, что они требуют более сложных схем квантования и калибровки, современные исследования показывают, что большие языковые модели могут эффективно работать даже в точности INT4.

Динамический батчинг

GPU - это устройство с массовым параллелизмом, большие батчи оно обрабатывает гораздо эффективнее, чем одиночные запросы: накладные расходы почти те же, а утилизация вычислительных блоков резко возрастает.

Проблема в том, что пользователи присылают запросы по одному и в случайное время.

Движки инференса (особенно в составе серверов типа Triton) используют динамический батчинг. Сервер не обрабатывает запрос мгновенно, а ждет буквально 1-2 миллисекунды, собирает пришедшие за это время запросы в "пачку" (батч), прогоняет их через GPU одновременно и раздает ответы.

Результат: задержка ответа для одного пользователя растет незначительно (на пару мс), но общая пропускная способность сервера вырастает в 5-10 раз.

Управление памятью

В наивной реализации память для тензоров выделяется и освобождается динамически во время выполнения. В высоконагруженном инференсе постоянные malloc и free фрагментируют память и тратят процессорное время.

Продвинутые движки (например, TensorRT) выделяют всю необходимую память при старте. Во время работы они просто переиспользуют заранее аллоцированные буферы. Это гарантирует предсказуемое потребление ресурсов и отсутствие внезапных падений из-за нехватки памяти в пике нагрузки.
Теперь посмотрим на инструменты, которые реализуют эти принципы на практике. И начнем с самого универсального.

ONNX Runtime (ORT)

Если вам нужна золотая середина между простотой внедрения и производительностью, оптимальный выбор - ONNX Runtime. Это открытый проект под эгидой Linux Foundation, который стал стандартом де-факто для кросс‑платформенного инференса моделей в формате ONNX.

Философия: Interoperability

Главная проблема, которую решает ORT - зависимость от фреймворка. Раньше, если вы обучили модель в PyTorch, вы были вынуждены тянуть весь тяжелый PyTorch в продакшен-контейнер. Если использовали TensorFlow - тянули его.

ONNX (Open Neural Network Exchange) - это промежуточное представление (Intermediate Representation, IR). Это стандартизированный формат графа, независимый от того, где модель была обучена. Вы экспортируете веса и архитектуру в файл .onnx (по сути, сериализованный Protobuf), который может быть выполнен ONNX Runtime и другими совместимыми движками.

Execution Providers: Киллер-фича

Главная сила ORT — модульная архитектура бэкендов, называемых Execution Providers (EP).
Код вызова модели остается неизменным, меняется только конфигурация:
  • CPUExecutionProvider: Использует оптимизации Intel MKL-DNN / OneDNN. Работает везде, идеально для простых сервисов.
  • CUDAExecutionProvider: Стандартное ускорение на NVIDIA GPU.
  • TensorrtExecutionProvider: ORT сам пытается скомпилировать подграфы через TensorRT (о котором ниже), а неподдерживаемые части автоматически выполняет через CUDAExecutionProvider.
  • CoreMLExecutionProvider: Для запуска на устройствах Apple (Mac/iPhone).

Экспорт и Запуск

Типичная последовательность (пайплайн) выглядит так. Сначала экспортируем модель (обычно из PyTorch):
import torch

# 1. Загружаем обученную модель
model = MyModel().cuda().eval()

# 2. Создаем dummy input (нужен для трассировки графа)
dummy_input = torch.randn(1, 3, 224, 224, device='cuda')

# 3. Экспорт
# dynamic_axes позволяет менять размер батча в рантайме
torch.onnx.export(
    model, 
    dummy_input, 
    "model.onnx", 
    input_names=["input"], 
    output_names=["output"],
    dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}},
    opset_version=17  # Версия набора операторов, критически важный параметр
)

Затем пишем код инференса (он не требует PyTorch, только легкую библиотеку `onnxruntime`):

import onnxruntime as ort
import numpy as np

# Выбираем провайдер: по классике сначала пытаемся на GPU, если нет - CPU
providers = [
    ('CUDAExecutionProvider', {
        'device_id': 0,
        'arena_extend_strategy': 'kNextPowerOfTwo',
        'cudnn_conv_algo_search': 'EXHAUSTIVE',
    }),
    'CPUExecutionProvider',
]

# Загрузка сессии (тяжелая операция, делается один раз при старте сервиса)
sess = ort.InferenceSession("model.onnx", providers=providers)

# Инференс
input_name = sess.get_inputs()[0].name
data = np.random.randn(8, 3, 224, 224).astype(np.float32) # батч 8
result = sess.run(None, {input_name: data})
  • Простота
    Переезд с PyTorch занимает минимум времени.
  • Графовые оптимизации
    ORT "из коробки" делает предварительный расчет константных веток графа (constant folding) и удаление ненужных операций (например, Identity layers).
  • Экосистема
    Работает на C++, C#, Java, Python, JS.
  • Opset Hell
    Стандарт ONNX обновляется. Если вы используете в PyTorch супер-новую функцию активации, её может не быть в текущей версии Opset ONNX. Придется ждать или писать кастомные операторы на C++.
  • if/else логика
    ONNX плохо переваривает циклы и условные переходы внутри модели. Такие модели при трассировке либо ломаются, либо фиксируются по одной ветке исполнения.

NVIDIA: TensorRT

TensorRT (TRT) - это полный набор инструментов для создания приложений (SDK) для высокопроизводительного инференса глубоких нейронных сетей исключительно на GPU NVIDIA. В отличие от фреймворков (PyTorch/TF) и интерпретаторов (ONNX Runtime), TensorRT — это компилятор.
Что происходит во время сборки
  • Автоматическая настройка ядер (Kernel Auto-Tuning)

    Это ключевая особенность. TRT прогоняет слои вашей сети через десятки различных реализаций алгоритмов (например, разные виды сверток cuDNN) на вашем железе. Он замеряет время исполнения и выбирает самую быструю реализацию именно для этой карты и этих размеров тензоров.
  • Слияние словаем (Вертикальность и горизонтальность) (Layer Fusion (Vertical & Horizontal))

    TRT агрессивно объединяет слои. Если у вас есть три ветки с одинаковой структурой (как в Inception-блоках), он может слить их в одно широкое ядро (kernel), чтобы загрузить GPU по максимуму.
  • Оптимизация памяти (Memory Optimization)

    TRT перестраивает использование памяти, создавая единый эффективный memory footprint, минимизируя аллокации.

Этап сборки

Обязательный этап оптимизации, в результате которого получается Engine файл ( .plan или .engine). Этот процесс необратим и привязан к вычислительной мощности (compute capability) видеокарты. Engine, собранный на NVIDIA T4, не запустится на NVIDIA A100.

Динамические размерности

Чистый TensorRT любит статику. Чтобы эффективно выделять память, нужно знать точные размеры тензоров заранее. Но в реальности мы хотим сделать размер батча динамическим. Для решения этого в TRT есть концепция Optimization Profiles.
При сборке движка вы должны явно задать три состояния для каждого входа:
  • min_shape: минимально возможный вход (например, batch=1).
  • opt_shape: оптимальный вход, под который TRT будет тюнить CUDA ядра сильнее всего (например, batch=32).
  • max_shape: предел, выше которого аллокатор не пойдет (например, batch=128).

Компиляция
(через утилиту trtexec)

Самый простой способ сконвертировать ONNX в TensorRT Engine - использовать официальную консольную утилиту trtexec, которая идет в комплекте с драйверами или докер-образом NVIDIA.
# Пример команды для сборки движка с поддержкой FP16

trtexec --onnx=model.onnx \
        --saveEngine=model.engine \
        --fp16 \
        --minShapes=input:1x3x224x224 \
        --optShapes=input:32x3x224x224 \
        --maxShapes=input:64x3x224x224 \
        --workspace=4096 # Разрешаем использовать 4ГБ памяти для поиска алгоритмов

Инференс

Запуск скомпилированного движка в Python требует работы с сырыми указателями памяти (через pycuda или встроенные байндинги). Ниже приведен простейший пример инференса. TRT - низкоуровневый инструмент, для работы с ним крайне рекомендуется изучить официальную документацию.
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit  # Автоматическая инициализация CUDA контекста
import numpy as np

# 1. Загрузка движка из файла
TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
with open("model.engine", "rb") as f, trt.Runtime(TRT_LOGGER) as runtime:
    engine = runtime.deserialize_cuda_engine(f.read())

# 2. Создание контекста исполнения и выделение памяти
context = engine.create_execution_context()

# Модель может иметь несколько входов/выходов, берем первый вход и выход
h_input = cuda.pagelocked_empty(trt.volume(engine.get_binding_shape(0)), dtype=np.float32)
h_output = cuda.pagelocked_empty(trt.volume(engine.get_binding_shape(1)), dtype=np.float32)

# Выделяем память на GPU (Device memory)
d_input = cuda.mem_alloc(h_input.nbytes)
d_output = cuda.mem_alloc(h_output.nbytes)

# Создаем поток для асинхронного выполнения
stream = cuda.Stream()

def predict(batch_input):
    # Копируем входные данные в "закрепленную" (pagelocked) память, затем на GPU
    np.copyto(h_input, batch_input.ravel())
    cuda.memcpy_htod_async(d_input, h_input, stream)
    
    # Исполнение: передаем адреса буферов на GPU
    context.execute_async_v2(bindings=[int(d_input), int(d_output)], stream_handle=stream.handle)
    
    # Перенос результата обратно в RAM хоста
    cuda.memcpy_dtoh_async(h_output, d_output, stream)
    
    # Ждем завершения операций в этом потоке
    stream.synchronize()
    return h_output.reshape(engine.get_binding_shape(1))

res = predict(my_numpy_array)
  • NLP-задачи
    У вас модель компьютерного зрения или классические NLP-задачи (BERT-like).
  • Фиксированное железо
    Вы точно знаете, на каком железе будет работать код.
  • Максимальная скорость
    Вам нужна максимальная скорость (часто x2-x4 по сравнению с PyTorch).
  • Docker-совместимость
    Вы готовы потратить время на подробное чтение документации и сложную сборку Docker-контейнеров (совместимость версий CUDA, TRT, драйверов).
  • Инференс большой языковой модели (LLM-инференс)
    Инференс LLM - для них есть более специализированные решения.
  • Фиксированное железо
    Логика модели сильно завязана на динамическое поведение (рекурсия, сложные циклы).
TensorRT даёт максимальную производительность и минимальную задержку, но ценой гибкости, переносимости и сложности внедрения.

NVIDIA Triton Inference Server

Если TensorRT — это двигатель болида Формулы-1, то Triton Inference Server — это пит-стоп и команда механиков. Запускать модель в голом скрипте (обычный predict()) можно, пока у вас ~10 запросов в секунду. Когда их становится больше, может потребоваться оркестрация.

Triton (ранее TensorRT Inference Server) решает главную архитектурную проблему:
эффективную утилизацию GPU при разнородной нагрузке.
Ключевые фичи:
  • Мултисерверная часть (мультибэкенд)

    Поддерживает одновременное выполнение моделей PyTorch, TensorFlow, ONNX, и TensorRT. Это избавляет от необходимости поднимать 5 разных микросервисов под разные фреймворки.
  • Параллельное выполнение моделей (Concurrent Model Execution)

    Вы можете загрузить 4 копии одной и той же модели (instance groups) в память одной видеокарты. Пока одна копия ждет данные из памяти, другая может исполняться. Это позволяет утилизировать CUDA ядра на 100%.
  • Динамический батчинг

    Как упоминалось во введении, Triton объединяет входящие запросы в батчи. Вы настраиваете окно ожидания (например, 5 мс), и сервер сам объединяет поступившие за это время тензоры.

Model Repository
и Конфигурация

Triton не требует написания серверного кода (обычно) и работает по декларативному принципу. Вы кладете модели в папку определенной структуры и пишете config.pbtxt (синтаксис protobuf).

Пример конфига для модели классификации:

name: "resnet50_onnx"
platform: "onnxruntime_onnx" // для TensorRT будет "tensorrt_plan"
max_batch_size: 128 // для TensorRT должен быть >= любому значению в dynamic_batching.preferred_batch_size

// Настройка входов
input [
  {
    name: "input_tensor"
    data_type: TYPE_FP32
    dims: [ 3, 224, 224 ]
  }
]

// Настройка выходов
output [
  {
    name: "softmax_output"
    data_type: TYPE_FP32
    dims: [ 1000 ]
  }
]

// Батчинг
dynamic_batching {
  preferred_batch_size: [ 16, 32 ]
  max_queue_delay_microseconds: 2000 // окно ожидания 2 мс
}

// Запустить 2 копии модели на GPU с ID 0
instance_group [
  {
    count: 2
    kind: KIND_GPU
    gpus: [ 0 ]
  }
]

Ансамбли моделей

Часто инференс — это цепочка, например:
Токенизация (CPU) -> Эмбеддинг (BERT) -> Реранкер (Cross-Encoder) -> Классификатор.

В обычном микросервисе вы бы передавали данные между функциями, копируя их в памяти. Triton позволяет определить Ensemble Model. Это виртуальная модель, которая описывает ацикличный граф потока данных (DAG) между моделями внутри сервера. Данные передаются через shared memory, без сетевых задержек.

Итог

Если у вас кластер Kubernetes, зоопарк моделей, сложные ансамбли и высокие нагрузки — Triton безальтернативен. Это стандарт индустрии для продакшен-деплоя.

Intel OpenVINO

Основная цель OpenVINO - максимальная эффективность инференса на Intel-платформах, особенно для CPU и встроенной графики.

Model Optimizer и IR

Похоже на TensorRT: вы берете модель (ONNX, PyTorch) и конвертируете её в промежуточное представление (Intermediate Representation, IR) - пару файлов .xml (архитектура) и .bin (веса).

В этот момент происходит оптимизация под архитектуру x86_64: слияние слоев, замена операций на векторные инструкции CPU/GPU/NPU (AVX-512, VNNI, AMX).
import torch
import torchvision.models as models
import openvino as ov

# 1. Загружаем модель
weights = models.ResNet50_Weights.DEFAULT
model = models.resnet50(weights=weights)
model.eval()

dummy_input = torch.randn(1, 3, 224, 224)

# 2. Конвертируем
ov_model = ov.convert_model(model, example_input=dummy_input)

# 3. Сохраненяем модель и веса (квантизирует модель в FP16 по умолчанию)
ov.save_model(ov_model, "resnet50.xml", compress_to_fp16=True)

Inference Engine

Код инференса в OpenVINO универсален, можно написать приложение один раз и менять устройство исполнения флагом:
from openvino.runtime import Core

core = Core()
# 1. Загружаем модель и веса
model = core.read_model(model="resnet50.xml", weights="resnet50.bin")
compiled_model = core.compile_model(model, device_name="CPU") 
# device_name может быть "GPU" (интегрированная графика Intel Iris), "NPU" (новые чипы Meteor Lake) или "AUTO"

input_layer = compiled_model.input(0)
output_layer = compiled_model.output(0)

# 2. Инференс
result = compiled_model([input_data])[output_layer]
Так же нельзя не упомянуть поддержку INT8 квантизации через Neural Compressor (INC). На современных CPU Intel Xeon (Sapphire Rapids) с инструкциями AMX квантизированные модели по производительности могут сравниться с выполнением на бюджетных дискретных GPU.
Плюсы
  • Стоимость инфраструктуры

    Использование уже имеющихся Xeon в сценариях, где не требуется экстремальный параллелизм GPU, например, для классификации видеопотока или обработки табличных данных, - это самое экономичное решение.
  • Гибкость

    Благодаря поддержке NPU и iGPU, OpenVINO подходит для набирающего популярность Edge AI. Одна и та же модель может выполняться на сервере, промышленном контроллере или офисном ноутбуке.
  • Производительность

    Начиная с Xeon Sapphire Rapids, инструкции AMX позволяют выполнять до 2048 операций INT8 за такт. В сочетании с квантованием через Neural Compressor это обеспечивает достаточную скорость для легких LLM и классических CV/NLP задач в реальном времени.
Для стека, базирующегося на Intel, и при ограниченном бюджете на GPU, OpenVINO + INT8 квантование обеспечивает эффективный, предсказуемый и экономичный инференс, закрывая большинство потребностей в классическом Computer Vision и NLP.

Итог: как выбрать движок и не прогадать

Мы прошли путь от инференса на чистом PyTorch до специализированных компиляторов и серверов оркестрации. Сегодня машинное обучение внедряется во всё большее число бизнес-процессов и технических систем, и именно инференс становится всё более ощутимой статьей расходов. Грамотный выбор технологии для инференса позволяет оптимизировать нагрузку на железо, снизить расходы на инфраструктуру и обеспечить стабильный пользовательский опыт при росте трафика и объёма данных.

При выборе стека стоит опираться на три ключевых фактора:

  • Железо

    • GPU NVIDIA: если важна каждая миллисекунда, оптимальным выбором будет TensorRT.
    • Ограниченный бюджет или Edge-устройства без дискретной графики: OpenVINO позволяет выжать максимум из процессоров Intel, включая CPU, iGPU и NPU.
  • Сложность пайплайна

    • Простые микросервисы с одной моделью: ONNX Runtime обеспечивает универсальность и легкость деплоя.
    • Много моделей, разные фреймворки или сложные ансамбли: NVIDIA Triton упрощает батчинг, очереди и конвейеры.
  • Этап развития продукта

    • MVP: часто достаточно TorchScript или ONNX с CPU-инференсом.
    • High-load / продакшен: оптимизация с квантованием INT8 и компилируемыми движками становится необходимой, позволяя сократить расходы на облачные GPU в 2–4 раза.
это всегда компромисс между скоростью разработки и производительностью (рантайма). Понимание основных механизмов его оптимизации позволяет выбрать баланс между стоимостью инстанса, задержкой ответа и сложностью разработки и поддержки.

Инференс в продакшене -

Нажимая на кнопку «Отправить», я подтверждаю, что согласен(на) с политикой конфиденциальности
Оставить заявку