← Усі статті

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 — до «віртуальної машини під конкретне залізо», 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, рекурентні блоки, нестандартне об’єднання ознак — часті джерела несумісних або неоптимальних ops. Постобробку CTC часто лишають поза графом (у Python і в JS окремо) — так простіше зберегти паритет і не тягнути в ONNX крихку логіку алфавіту.

Детектори (YOLO і родина). Голови, декодування рамок, придушення немаксимумів (NMS): частина команд експортує лише основу мережі, а постобробку тримає зовні; частина намагається вшити все в граф. Для браузера другий шлях важчий за ops і налагодженням. Інженерний контекст детектора — у розборі YOLO.

Кастомні CUDA-ядра / нестандартні модулі. Якщо шару немає в ONNX opset, експорт не «трохи підправить» — потрібна заміна на склад зі стандартних ops або відмова від цього шляху для клієнта.

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

Паритет: як довести, що експорт чесний

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

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

Розбіжність «у Colab 0.98, у вкладці сміття» майже завжди тут: інше масштабування, RGB vs BGR, /255 vs ImageNet mean, інше квантування. 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 у межах допуску
  • Препроцес і постпроцес описані поруч з артефактом (мовно-агностично)
  • Для браузера: перевірка на цільовому provider (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/провайдер і на якому зелений паритет. Ганятися за максимальним номером заради новизни не варто.

Чому експорт успішний, а браузер падає?

Часто ops є на CPU-провайдері ORT, але немає або інакше реалізовані на WebGPU. Перевіряйте саме web-провайдер.

Чи потрібно конвертувати модель знову після кожного навчання?

Так, якщо змінилися ваги або архітектура. Той самий скрипт експорту має проганятися в CI або принаймні в чеклісті релізу: новий чекпоінт → новий .onnx → паритет → публікація артефакту. Інакше прод довго живе на старому графі, поки в репозиторії вже інша точність.

Чи можна навчати в ONNX?

Звичайний сценарій — ні: навчають у PyTorch тощо, ONNX використовують для виводу. Є експериментальні суміжні потоки, але для прод-пайплайну «навчання → експорт → ORT» лишається нормою.

Далі за темою

Висновок

ONNX — це спосіб зафіксувати обчислювальний граф моделі так, щоб його могли виконувати різні рушії. ONNX Runtime — найпряміший шлях від цього файлу до CPU, GPU сервера або WebGPU у вкладці. Експорт із PyTorch — інженерний контракт: opset, форми, препроцес, паритет, версії. Закрийте цей контракт — і клієнтський AI перестає бути лотереєю; поки граф нечесний, жоден device: 'webgpu' не врятує.

Коментарі

Завантаження коментарів…