Logo Craft Homelab Docs Контакты Telegram
ONNX: универсальный формат — конвертация, оптимизация
Sat Jan 24 2026

ONNX: универсальный формат

Когда вы тратите недели на тонкую настройку модели в TensorFlow, а затем вас просят запустить её в продакшене на устройстве с CoreML — это момент, когда вы начинаете ненавидеть проприетарные форматы. Каждый фреймворк запирает ваш труд в своей экосистеме, как дракон сокровищ. ONNX — это не просто ещё один формат; это попытка создать универсальный язык для нейросетей, который позволяет перенести вашу модель из одной среды в другую без переобучения. Но работает ли это на практике? Давайте разбираться без розовых очков.

Универсальность vs. Производительность: Цена свободы

ONNX (Open Neural Network Exchange) — это не просто сериализатор моделей. Это полноценная вычислительная среда с операциями, типами данных и графом вычислений. Когда вы конвертируете модель из TensorFlow в ONNX, вы не просто меняете упаковку — вы перестраиваете вычислительный граф с использованием другого набора примитивов.

# Пример конвертации модели PyTorch в ONNX
import torch
import torch.onnx

# Создадим простую модель для демонстрации
model = torch.nn.Sequential(
    torch.nn.Linear(10, 20),
    torch.nn.ReLU(),
    torch.nn.Linear(20, 2)
)

# Укажем входные данные для трассировки
dummy_input = torch.randn(1, 10)

# Экспорт в ONNX
torch.onnx.export(model,               # экспортируемая модель
                 dummy_input,          # входные данные для модели
                 "model.onnx",         # путь для сохранения выходного файла
                 export_params=True,   # сохраните обученные параметры веса
                 opset_version=11,     # версия ONNX
                 do_constant_folding=True,  # оптимизация для постоянных
                 input_names=['input'],   # имя входного слоя
                 output_names=['output'],  # имя выходного слоя
                 dynamic_axes={'input' : {0 : 'batch_size'},    # динамические размерности
                             'output' : {0 : 'batch_size'}})

Этот код экспортирует вашу модель в ONNX, но за этой простотой скрывается множество нюансов. Во-первых, PyTorch использует трассировку (trace) для создания вычислительного графа, что может не уловить все случаи, особенно модели с управляющими потоками (control flow). Во-вторых, версия opset_version критически важна — она определяет, какие операции будут доступны в целевой среде. Я однажды столкнулся с ситуацией, когда модель, которая работала идеально с opset 10, при экспорте с версией 11 давала результаты с точностью до 7 знака вместо 15. Казалось бы, мелочь, но для научных вычислений это катастрофа.

Более глубокая проблема — это семантическое различие между операциями в разных фреймворках. Например, операция Dropout в PyTorch по умолчанию активна во время обучения, а в TensorFlow — во время инференса. При конвертации это может привести к совершенно разному поведению модели. Разработчики ONNX пытаются решить эту проблему через “определяемые производителем” (vendor-specific) атрибуты, но это лишь частичное решение.

Внутренняя кухня ONNX: Операции и оптимизации

Когда мы говорим о ONNX, мы на самом деле говорим о двух вещах: модели как наборе параметров и вычислительном графе. Второе часто упускают из виду. ONNX Runtime — это не просто интерпретатор, это полноценный движок с оптимизациями.

import onnxruntime as ort
import numpy as np

# Создание сессии с оптимизациями
sess_options = ort.SessionOptions()
sess_options.execution_mode = ort.ExecutionMode.ORT_PARALLEL
sess_options.optimized_model_filepath = "optimized_model.onnx"

# Загрузка модели
ort_session = ort.InferenceSession("model.onnx", sess_options)

# Запуск инференса
input_data = np.random.randn(1, 10).astype(np.float32)
outputs = ort_session.run(
    None,  # имена выходных тензоров
    {'input': input_data}
)

Ключевая магия здесь — в оптимизации графа. ONNX Runtime выполняет несколько оптимизаций:

  1. Фузия операций (fusion) — объединение нескольких операций в одну
  2. Оптимизация памяти — сокращение аллокаций
  3. Параллелизм — выполнение независимых операций одновременно

Но есть и обратная сторона: эти оптимизации выполняются при загрузке модели, что увеличивает время первого инференса (cold start). Для serverless это может быть проблемой. Я работал над проектом, где cold start занимал 800мс из-за сложных оптимизаций, что неприемлемо для low-latency API. В итоге мы отключили часть оптимизаций, сократив cold start до 200мс, ценой 15% пропускной способности.

Что многие не понимают — ONNX Runtime использует JIT-компиляцию для некоторых операций, что добавляет ещё один слой сложности. В зависимости от платформы, могут использоваться разные бэкенды: CUDA для GPU, TensorRT для NVIDIA, OpenVINO для Intel и т.д. Это означает, что производительность может кардинально отличаться на разных устройствах. На одном проекте мы столкнулись с тем, что одна и та же модель работала на 30% быстрее на CPU с OpenVINO, чем на CUDA — результат, который трудно предсказать без тестирования.

Узкие места, о которых молчат: Производительность в реальном мире

Проблема производительности — самая больная тема в экосистеме ONNX. Конвертация не всегда сохраняет все нюансы исходной реализации.

Проблема несоответствия реализации операций: Разница в точности вычислений между разными рантаймами. Например, операция softmax может реализовываться по-разному в ONNX Runtime и TensorFlow, приводя к небольшим, но критичным для некоторых задач расхождениям.

Оптимизации ONNX Runtime: Иногда ухудшают производительность для специфических моделей. В одном проекте мы столкнулись с тем, что активация LeakyReLU в ONNX работала на 20% медленнее, чем в оригинальной реализации TensorFlow, из-за особенностей фузии операций.

Ограниченная поддержка кастомных операций: Невозможность экспорта сложных моделей. Однажды мы не смогли экспортировать модель с кастомным attention механизмом, так как он не имел стандартной реализации в ONNX. Пришлось либо переписывать слой на нативный код рантайма, либо отказываться от ONNX.

Пример проблемы с округлением:

# Исходная модель в TensorFlow
def custom_op(x):
    return tf.round(x * 100) / 100

# Конвертированная в ONNX
# Округление может работать по-разному в разных рантаймах
# из-за различий в реализации математических операций

Ещё один неприятный сюрприз — управление памятью. ONNX Runtime не так агрессивно использует GPU, как нативные реализации. Для моделей, чувствительных к пропускной способности памяти, это может стать узким местом. В нашем проекте с обработкой видео в реальном времени, ONNX показал в 2 раза меньшую пропускную способность на GPU по сравнению с TensorFlow, заставив нас отказаться от универсальности ради производительности.

Но есть и более глубокая проблема: векторизация операций. В то время как TensorFlow и PyTorch имеют highly оптимизированные реализации для различных операций на GPU, ONNX Runtime часто полагается на более общие реализации, которые могут не использовать специфические возможности GPU. Например, умножение матриц в TensorFlow может использовать специализированные библиотеки вроде cuBLAS, в то время как ONNX Runtime может использовать более общую реализацию, что приводит к снижению производительности.

Практические сценарии: Когда ONNX спасает, а когда подводит

ONNX — ваш выбор, если:

  • Вам нужно развернуть модель на разных платформах (iOS, Android, Windows, Linux)
  • У вас есть команда, работающая с несколькими фреймворками
  • Вы готовы пожертвовать 5-10% производительности ради универсальности
  • Ваша модель использует стандартные операции без сложных кастомных слоев
  • Вы разрабатываете продукт, который должен работать в мультиклаудовой среде

Избегайте ONNX, когда:

  • Критически важна максимальная производительность
  • Модель использует специфические операции фреймворка
  • Вам нужна поддержка новейших архитектур нейросетей (они часто появляются сначала в одном фреймворке)
  • У вас нет ресурсов на тестирование конвертированной модели
  • Вы работаете с моделями, чувствительными к точности вычислений (например, научные вычисления с двойной точностью)

Особенно стоит отметить сценарий edge-устройств. Для мобильных устройств ONNX может быть не лучшим выбором из-за размера рантайма и меньшей оптимизации под конкретные железки. В одном из наших проектов мы получили на 40% более быстрый инференс на iPhone, используя CoreML вместо ONNX, несмотря на универсальность последнего.

Выводы: Универсальность как компромисс

ONNX — это не серебряная пуля, а прагматичный компромисс между универсальностью и производительностью. Для многих компаний, работающих в мультиклаудовой среде, этот компромисс выгоден. Но если вы строите высоконагруженный сервис, где каждая миллисекунда и каждый процент CPU/GPU имеют значение, нативные реализации могут оказаться лучше.

Самая большая ценность ONNX — это не просто конвертер моделей, а стандарт для обмена вычислительными графами. Это как TCP/IP для нейросетей — не самый быстрый протокол, но тот, что связывает разные миры вместе. И как и в случае с TCP/IP, иногда приходится мириться с накладными расходами ради универсальности.

В итоге решение использовать ONNX — это всегда вопрос баланса между гибкостью и производительностью, и нет универсального ответа, подходящего для всех сценариев. Если вы цените универсальность больше производительности и у вас есть ресурсы на тестирование — ONNX может стать отличным выбором. В противном случае — возможно, стоит остаться в экосистеме одного фреймворка.