Прямой ответ
Инференс на CPU оправдан, когда GPU нет или его аренда дороже, чем выигрыш: небольшие модели, непостоянная нагрузка, работа на устройстве пользователя, требования к стоимости. Два инструмента закрывают большинство задач:
- ONNX Runtime — кроссплатформенный движок выполнения моделей в формате ONNX с графовыми оптимизациями и разными execution providers.
- OpenVINO — стек Intel для оптимизации и выполнения на CPU (и iGPU): компиляция графа, квантование INT8, планирование под конкретную микроархитектуру.
Типовой путь: обучили модель в PyTorch → экспортировали в ONNX → при необходимости сконвертировали и оптимизировали в OpenVINO → развернули в сервисе.
Экспорт из PyTorch в ONNX:
import torch
model.eval()
dummy = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model, dummy, "model.onnx",
input_names=["input"], output_names=["logits"],
dynamic_axes={"input": {0: "batch"}, "logits": {0: "batch"}},
opset_version=17,
)
Запуск через ONNX Runtime:
import onnxruntime as ort, numpy as np
so = ort.SessionOptions()
so.intra_op_num_threads = 4 # потоки внутри операции
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
sess = ort.InferenceSession("model.onnx", so, providers=["CPUExecutionProvider"])
out = sess.run(None, {"input": np.random.randn(1, 3, 224, 224).astype("float32")})
Что даёт ускорение на CPU:
- Оптимизация графа. Слияние операций, устранение избыточных преобразований, постоянная свёртка.
- Квантование INT8. Уменьшает объём читаемых весов и использует целочисленные инструкции; даёт прирост на задачах, ограниченных памятью.
- Правильное число потоков.
intra_op_num_threadsобычно равен числу физических ядер; завышение приводит к конкуренции и падению скорости. - Фиксированная форма входа. Динамические размеры мешают части оптимизаций; если batch известен заранее, фиксируйте его.
Практика: настройка и проверка
Квантование в OpenVINO (INT8, пост-тренировочное):
import openvino as ov
import nncf
core = ov.Core()
model = core.read_model("model.xml")
def transform_fn(data_item):
return {"input": data_item}
quantized = nncf.quantize(model, calibration_dataset, model_transformation_fn=transform_fn)
ov.save_model(quantized, "model-int8.xml")
Калибровочный набор — несколько сотен представительных входов. Без него квантование даёт заметную потерю точности.
Проверка, что оптимизация не сломала качество:
import numpy as np
def compare(orig, opt, samples):
diffs = []
for x in samples:
a = orig.run(None, {"input": x})[0]
b = opt.run(None, {"input": x})[0]
diffs.append(np.abs(a - b).max())
return max(diffs), np.mean(diffs)
Для классификации дополнительно сверяют долю совпадений предсказанных классов: падение точности больше 1 % — сигнал пересмотреть калибровку.
Рекомендации:
- Измеряйте до и после. Фиксируйте задержку (p50, p95) и пропускную способность на одинаковом наборе входов; как корректно работать с перцентилями — в статье про гистограммы метрик.
- Не смешивайте настройки потоков в нескольких процессах. Если сервис запускает несколько экземпляров модели, общее число потоков не должно превышать число физических ядер.
- Проверяйте троттлинг. На ноутбуках длительная нагрузка снижает частоты; измерение «одного запроса» даёт нерепрезентативный результат. Диагностика — в статье про производительность Linux-сервера.
- Для классических ML-задач часто достаточно ONNX Runtime. Если модель не LLM и не требует специфичных оптимизаций Intel, переход на OpenVINO может не дать выигрыша. Контекст работы без GPU — в статье про sklearn без GPU.
Как организовать измерения, чтобы им можно было верить. Прогрейте модель перед замером: первые вызовы включают компиляцию и аллокации, поэтому их время в разы выше установившегося. Затем выполняйте не менее нескольких сотен запросов и смотрите на распределение, а не на среднее: на CPU заметны выбросы из-за планировщика ОС, фоновых задач и теплового троттлинга. Измеряйте при той же конкуренции, которая будет в продакшене: одиночные запросы почти всегда быстрее, чем поток, потому что не конкурируют за потоки выполнения и кэш.
Типичные ошибки развёртывания. Первая — создание новой сессии инференса на каждый запрос: инициализация дорогая, сессию создают один раз и переиспользуют. Вторая — несколько процессов, каждый со своим числом потоков, из-за чего общее число потоков превышает число ядер и скорость падает у всех. Третья — отсутствие таймаута на уровне сервиса: зависший запрос без ограничения занимает поток выполнения и постепенно исчерпывает пул.
Практический сценарий: сервис классификации не укладывается в SLA
Контекст: классификатор документов на CPU, 300 мс на запрос при требовании 150 мс; нагрузка 20 RPS на 8-ядерный сервер.
- Диагностика. Профилирование показывает, что 70 % времени занимает чтение весов и матричные умножения в FP32; число потоков было выставлено в 16 при 8 физических ядрах.
- Первые меры. Устанавливаем
intra_op_num_threads = 8, включаем полную оптимизацию графа. Задержка падает до 240 мс — недостаточно. - Квантование. Выполняем INT8-квантование с калибровкой на 400 реальных документах; проверяем качество: совпадение классов 99.3 %, максимальное расхождение логитов в допуске.
- Результат. Задержка p95 — 95 мс, пропускная способность выросла с 8 до 26 RPS на том же железе; SLA выполняется с запасом.
- Профилактика. Добавляем нагрузочный тест в CI, который проверяет p95 на эталонном железе; сравнение с альтернативами по стоимости фиксируем в ADR.
Вывод: на CPU выигрыш дают три вещи — оптимизация графа, квантование и корректное число потоков. Добавление ядер без этих мер почти не помогает.
FAQ
Что выбрать: ONNX Runtime или OpenVINO? ONNX Runtime — универсальный выбор и хорошая точка старта. OpenVINO оправдан на процессорах Intel, когда нужно максимальное быстродействие и зрелое INT8-квантование. Сравнение стоит проводить на своей модели и своём профиле нагрузки, а не по общим заявлениям.
Когда CPU проигрывает GPU настолько, что это критично? Для больших моделей и высоких batch GPU выигрывает кратно. Для моделей до сотен мегабайт и нагрузки в единицы-десятки RPS оптимизированный CPU часто достаточен и дешевле в эксплуатации.
Как квантование влияет на LLM? Для больших языковых моделей применяются свои форматы и схемы — GGUF, AWQ, GPTQ; они отличаются от классического INT8-квантования классификаторов. Подробный разбор — в статье про квантование LLM.
Нужно ли пересобирать модель при смене процессора? Для OpenVINO компиляция графа выполняется под конкретное устройство, поэтому при смене платформы модель нужно пересобрать (или компилировать на старте сервиса). Для ONNX Runtime переносимость выше: один файл работает на разных платформах, но уровень оптимизации может отличаться.
Источники: OpenVINO docs, ONNX, ONNX Runtime performance tuning — ссылки в шапке.