← Все статьи

ONNX: от PyTorch до Runtime — формат, экспорт и паритет вывода

Что такое ONNX и ONNX Runtime, как экспортировать модель из PyTorch, opset и динамические оси, почему ломается экспорт CRNN/YOLO и как сверить вывод с Python перед браузером.

ONNX: от PyTorch до Runtime — формат, экспорт и паритет вывода
Содержание

В статье про WebGPU и Transformers.js цепочка выглядит коротко: модель → ONNX → Runtime → GPU в браузере. На практике именно среднее звено съедает дни: экспорт из PyTorch падает на незнакомом операторе, в браузере цифры не совпадают с Colab, а «просто сохрани .onnx» не объясняет, что внутри файла. Ниже — полный разбор: чем формат отличается от движка, как устроен граф и opset, как экспортировать и проверять паритет, что ломается у CRNN и детекторов, и куда ведёт путь к ONNX Runtime Web.

Ключевые выводы

ONNX — формат графа, не фреймворк обучения. Обучаете в PyTorch (или другом каркасе), в ONNX экспортируете переносимый артефакт.

ONNX Runtime — движок исполнения. Он читает граф и гоняет его на CPU, CUDA, WebAssembly, WebGPU и других провайдерах.

Opset фиксирует набор операций. Новый opset ≠ автоматически лучше для браузера: важна поддержка целевого провайдера исполнения.

Паритет важнее «зелёного» экспорта. Файл создался — ещё не успех. Сверяйте логиты и метрики с Python на том же препроцессе.

Браузерный путь начинается здесь. Без честного ONNX-артефакта клиентский AI на WebGPU не заработает стабильно.

Зачем нужен ONNX

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

ONNX (Open Neural Network Exchange) как раз такой язык: открытая спецификация графа нейросети. Модель из PyTorch, иногда из других каркасов, превращается в файл (часто .onnx плюс внешние веса), который затем читает ONNX Runtime, конвейеры вроде TensorRT или браузерный ONNX Runtime Web.

Без ONNX типичные тупики:

  • «Модель есть только как state_dict — как отдать фронту?»
  • «На сервере CUDA, в браузере WebGPU — две разные сборки кода?»
  • «В Transformers.js модель из Hub уже в ONNX — а свою CRNN куда?»

Формат vs Runtime vs обучение

Три сущности, которые путают чаще всего:

Понятие Что это Чего это не делает
ONNX Спецификация и сериализация графа (операции, тензоры, константы) Не обучает модель; не выбирает GPU сам
ONNX Runtime Движок, исполняющий граф через провайдеры (CPU, CUDA, Web…) Не заменяет цикл обучения
PyTorch / TF Каркасы обучения и исследования Не обязаны совпадать с браузерным API

Аналогия: ONNX ближе к «промежуточному представлению программы», Runtime — к «виртуальной машине / JIT под конкретное железо», PyTorch — к языку, на котором вы писали исходник.

В экосистеме Hugging Face для браузера модели часто уже лежат как ONNX-артефакты. Transformers.js подтягивает их и отдаёт в ONNX Runtime Web. Свою модель вы готовите тем же путём: экспорт → проверка → только потом WebGPU.

Как выглядит файл на диске

На практике «модель в ONNX» — не всегда один файл. Часто это:

  • один .onnx, где граф и веса внутри;
  • или граф плюс внешние файлы весов (удобнее для больших сетей и частичной подгрузки).

В карточке модели на Hugging Face для браузера обычно лежат уже подготовленные артефакты под ONNX Runtime Web. Свою CRNN вы кладёте в свой CDN или статику приложения и сами отвечаете за версию и кеш. Имеет смысл рядом держать короткий MODEL.md: opset, формы входа, препроцесс, алфавит (если OCR), хеш файла, дата экспорта.

Полезный ритуал перед мержем: открыть граф в Netron, убедиться, что вход называется так, как ждёт фронт, и что на выходе ожидаемые логиты, а не «сюрприз» из промежуточного слоя.

Граф, операторы и opset

Граф ONNX — набор узлов. Узел — операция (Conv, MatMul, Softmax, LSTM, …) с входами и выходами-тензорами. Версия набора операторов называется opset (например, opset 17). Экспорт из PyTorch выбирает opset; рантайм должен уметь реализовать все узлы этого набора на выбранном провайдере.

Отсюда сюрпризы:

  • Экспорт на свежий opset прошёл, а ONNX Runtime Web + WebGPU не знает редкий op → падение или тихий уход на другой путь.
  • Кастомный слой в PyTorch без стандартного аналога → экспорт требует переписать через примитивы или символьные функции экспорта torch.onnx.
  • Одна и та же математика может разложиться в разные последовательности операций — поведение численно близкое, но не бит-в-бит.

Инструменты вроде Netron помогают увидеть граф: какие входы, какие узлы, где «чёрный ящик». Для отладки экспорта это обязательный шаг, не украшение.

Экспорт из PyTorch

Типичный путь (упрощённо):

import torch

model.eval()
dummy = torch.randn(1, 1, 32, 128)  # BCHW под ваш препроцесс

torch.onnx.export(
    model,
    dummy,
    "crnn.onnx",
    input_names=["input"],
    output_names=["logits"],
    opset_version=17,
    dynamo=False,  # уточняйте API своей версии PyTorch
)

На практике важнее параметров «красоты» три решения:

  1. model.eval() и без прореживания (dropout) / шумных веток — иначе граф и числа плывут.
  2. dummy той же формы и семантики, что прод (каналы, высота кропа, длина).
  3. Явные имена входов/выходов — чтобы препроцесс в JS не гадал порядок тензоров.

В новых версиях PyTorch путь экспорта эволюционирует (torch.onnx.export, режимы на базе Dynamo). Смотрите документацию вашей версии и фиксируйте её в файле описания модели: «экспортировано на torch X.Y, opset Z».

После экспорта сразу:

# проверка загрузки графа (пример)
python -c "import onnx; onnx.checker.check_model(onnx.load('crnn.onnx'))"

И прогон через onnxruntime в Python до разговоров про React.

Динамические оси и формы

Многие модели принимают переменный размер пакета или длину последовательности. В ONNX это динамические оси (dynamic_axes при экспорте): например, размер пакета и ширина кропа свободны, высота фиксирована под архитектуру CRNN.

Ошибки здесь типичны:

  • Заэкспортировали только размер пакета 1, W=128 — в браузере другой кроп падает.
  • Сделали все оси динамическими — часть провайдеров медленнее или хуже оптимизирует.
  • В JS создали тензор в порядке NHWC, а граф ждёт NCHW — «модель тупит», хотя веса верные.

Правило: зафиксируйте контракт формы в карточке модели (как у API). Браузерный код обязан повторить его один в один.

Что ломается на CRNN, YOLO и кастомных слоях

CRNN / последовательности. CTC, рекуррентные блоки, нестандартное объединение признаков — частые источники несовместимых или неоптимальных операций. Постобработку CTC часто оставляют вне графа (в Python и в JS отдельно) — так проще сохранить паритет и не тащить в ONNX хрупкую логику алфавита.

Детекторы (YOLO и семейство). Головы, декодирование рамок, подавление немаксимумов (NMS): часть команд экспортирует только основу сети, а постобработку держит снаружи; часть пытается вшить всё в граф. Для браузера второй путь тяжелее по операциям и отладке. Инженерный контекст детектора — в разборе YOLO.

Кастомные CUDA-ядра / нестандартные модули. Если слоя нет в ONNX opset, экспорт не «чуть подправит» — нужна замена на состав из стандартных ops или отказ от этого пути для клиента.

Практичный критерий готовности: на контрольном наборе кропов совпадают строки OCR или пересечение рамок (IoU) между PyTorch и onnxruntime в Python. Пока нет — в WebGPU рано.

Паритет: как доказать, что экспорт честный

Минимальный стенд:

  1. Один и тот же препроцесс (нормализация, масштабирование, порядок каналов) в функциях, общих для теста.
  2. Прогон PyTorch → логиты / строки.
  3. Прогон onnxruntime.InferenceSession → те же метрики.
  4. Допуски: для логитов — allclose с разумным atol/rtol; для OCR — доля полного совпадения строк на эталонном наборе.
  5. Фиксация версий: torch, onnx, onnxruntime, opset, хеш файла модели.

Расхождение «в Colab 0.98, во вкладке мусор» почти всегда здесь: другое масштабирование, RGB vs BGR, деление на 255 против среднего ImageNet, другое квантование. WebGPU ни при чём, пока вывод через onnxruntime в Python уже не совпал с PyTorch.

Связка с полевой практикой компактного OCR — CRNN на замерах УЗТ.

Мини-сценарий: от .pt до цифры в UI

Соберите узкий контур на одном типе кропа (например, ячейка замера):

  1. Обученная модель в PyTorch, eval(), фиксированный препроцесс.
  2. Скрипт экспорта в ONNX с именами входов/выходов и выбранным opset.
  3. Скрипт сравнения: 50–200 эталонных кропов → строки или логиты в PyTorch и в onnxruntime.
  4. Тот же препроцесс на TypeScript, загрузка через ONNX Runtime Web в рабочем потоке.
  5. Сначала провайдер WASM, затем попытка WebGPU с запасным путём.
  6. В UI — время холодной загрузки, тёплого вывода и флаг провайдера.

Так вы получаете измеримый «вертикальный срез» вместо абстрактного «перенесём все модели в браузер». Именно этот срез обычно убеждает заказчика лучше слайда про WebGPU.

Квантование и размер артефакта

Квантование (половинная точность, целые 8 бит и др.) уменьшает файл и часто ускоряет вывод, ценой качества. Его можно делать до экспорта, при экспорте или инструментами ORT — важно, какой путь выбрали и что именно попадёт в браузер.

Для клиента размер — продуктовая метрика: сотни мегабайт на первый визит бьют по холодному старту, о чём подробно в статье про ИИ в браузере. Правило то же: после квантования заново прогоните эталонный набор.

Провайдеры Runtime и путь в браузер

ONNX Runtime выбирает провайдер исполнения: CPU, CUDA, TensorRT (где настроено), в браузере — WASM и WebGPU. Один .onnx теоретически переносим; на практике список поддержанных операций и производительность отличаются.

Цепочка в продукт:

PyTorch → export ONNX → ORT Python (паритет)
                ↓
        `ONNX Runtime Web` + WASM (широкий охват)
                ↓
        `ONNX Runtime Web` + WebGPU (ускорение)
                ↓
     `Transformers.js` или прямой ORT API

Прямой вызов ORT в рабочем потоке уместен для своей модели без конвейера Hugging Face. Transformers.js удобен, когда модель уже в каталоге моделей и нужен стандартный конвейер. Оба варианта описаны в контексте UI в WebGPU + Transformers.js.

Чек-лист поставки .onnx в прод

  • Зафиксированы версии torch / onnx / onnxruntime / opset
  • Скрипт экспорта в репозитории, не «руками с ноутбука»
  • model.eval(), детерминированный пример входа под прод-формы
  • Динамические оси осознанны и задокументированы
  • onnx.checker и загрузка в ORT Python проходят
  • Эталонный набор: паритет с PyTorch в пределах допуска
  • Препроцесс и постпроцесс описаны рядом с артефактом (язык-агностично)
  • Для браузера: проверка на целевом провайдере (WASM / WebGPU)
  • Хеш файла и канал обновления модели (CDN / сброс кеша)
  • Понятно, что не входит в граф (декодирование CTC, NMS, правила)

Связь с серверным выводом

Даже если конечная цель — браузер, серверный ORT остаётся полезным эталоном. На CPU или CUDA проще поймать расхождения операций, сравнить скорость и решить, стоит ли вообще тащить модель на клиент. Иногда итог честный: модель остаётся на сервере, а в браузере живёт только лёгкий классификатор или проверка качества кадра.

Гибрид выглядит так: тяжёлый детект или большая сеть — API; компактный OCR кропа — локально после того, как сервер (или лёгкий клиентский детектор) отдал рамку. ONNX здесь общий язык для обоих контуров: один экспорт, два провайдера, одна таблица паритета.

Типичные ошибки

Считать ONNX ускорителем. Формат сам по себе не ускоряет; ускоряет железо и провайдер Runtime.

Экспортировать режим обучения. Dropout и ветки обучения портят граф и метрики.

Игнорировать препроцесс. Самая частая причина «браузер врёт».

Вшивать всю постобработку в граф без нужды. Усложняет экспорт и перенос алфавита/порогов.

Не фиксировать opset и версии. Через полгода «тот же скрипт» даёт другой файл.

Идти в WebGPU до паритета в Python. Отладка в DevTools дороже, чем numpy.allclose.

Отдельно договоритесь с командой фронтенда о канале обновления: смена opset или квантования — это новый артефакт, а не «тихий» перезапись на CDN. Иначе у части пользователей в кеше старый граф, у части — новый, а отладка превращается в спор «у меня работает». Семантическое версионирование файла модели (crnn-v3-opset17.onnx) и явный сброс кеша в адресе экономят недели.

Частые вопросы

ONNX — это библиотека Python?

Нет. Это стандарт формата графа. В Python ставят пакеты onnx (работа с моделью) и onnxruntime (исполнение).

Чем ONNX отличается от ONNX Runtime?

Формат vs движок. Можно иметь .onnx и исполнять его разными рантаймами; ORT — самый распространённый движок в этом стеке.

Нужен ли Transformers.js, если есть свой .onnx?

Нет. Можно грузить модель напрямую через ONNX Runtime Web. Transformers.js удобен для конвейеров Hugging Face и готовых карточек моделей.

Какой opset выбрать?

Тот, который стабильно поддерживает ваш целевой Runtime/провайдер и на котором зелёный паритет. Гнаться за максимальным номером ради новизны не стоит.

Почему экспорт успешен, а браузер падает?

Часто операции есть на CPU-провайдере ORT, но нет или иначе реализованы на WebGPU. Проверяйте именно web-провайдер.

Нужно ли конвертировать модель заново при каждом обучении?

Да, если изменились веса или архитектура. Тот же скрипт экспорта должен прогоняться в CI или хотя бы чеклистом релиза: новый чекпоинт → новый .onnx → паритет → публикация артефакта. Иначе прод долго живёт на старом графе, пока в репозитории уже другая точность.

Можно ли обучать в ONNX?

Обычный сценарий — нет: обучают в PyTorch и т.п., ONNX используют для вывода. Есть экспериментальные и смежные потоки, но для прод-пайплайна «обучение → экспорт → ORT» остаётся нормой.

Дальше по теме

Заключение

ONNX — это способ зафиксировать вычислительный граф модели так, чтобы его могли исполнять разные движки. ONNX Runtime — самый прямой путь от этого файла к CPU, GPU сервера или WebGPU во вкладке. Экспорт из PyTorch — инженерный контракт: opset, формы, препроцесс, паритет, версии. Закройте этот контракт — и клиентский AI перестаёт быть лотереей; пока граф нечестен, никакой device: 'webgpu' не спасёт.

Комментарии

Загрузка комментариев…