Part 4: Edge Deployment of an 86M Parameter Audio Transformer

від

у

Частина 4: Розгортання на краю моделі Audio Transformer з 86M параметрів

https://ift.tt/wvr3nUh

Пост 3 закінчився трьома числами: 97.4% точність на тесті, 100% прецизність, 0 хибнопозитивних. Усі вимірювання зроблені на MacBook.

Цього було достатньо, щоб довести, що модель може навчитися на перше “трещитки”. Але це не підтверджувало, що модель зможе працювати там, де мені потрібно: поряд з обсмажувачем кави, на Raspberry Pi 5, з USB-мікрофоном під час живого обсмаження.

Перший запуск на Pi був “це начебто працює”. Експорт PyTorch-контрпойнту в ONNX FP32, копіювання 345МБ файлу, запуск інференсу. Результат — 9.4 секунди на 10-секундному вікні аудіо. Це не помилка. Це вартість запуску трансформера з 86M параметрів на одному ядрі ARM Cortex-A76 з повною точністю FP.

Наступна спроба виявила іншу проблему. Більшої кількості потоків — швидше модель, але Pi лаявся. Оз був уже у SSH-сесії, то наступна команда була vcgencmd get_throttled. Відповідь прийшла як 0x50000: зменшення напруги та пригальмовування.

Тобто робота була не просто “експорт до ONNX”. Це стала невелика історія розгортання на краю: зменшити модель, налаштувати порогове значення, обмежити потоки, замінити джерело живлення, додати активне охолодження, а потім визначити, чи залишкова затримка й далі узгоджується з фізикою обсмаження кави.

Останнє число — 2.09 секунди на 10-секундному вікні за 4 потоками з активним охолодженням. Продукційний профіль використовує 2 потоки і становить 2.45 секунди, залишаючи вільний CPU-ресурс для решти системи.

У цьому пості

  • Що ми розгортаємо
  • Робочий процес на Pi
  • Експорт ONNX
  • Числа платформи
  • Період скошування порогу
  • Історія з апаратним забезпеченням
  • Висновок
  • Посилання
  • Посилання на джерела

Що ми розгортаємо

Модель — Audio Spectrogram Transformer, донавчена для бінарної класифікації: first_crack проти no_first_crack на 10-секундних вікнах аудіо, переприсэмпльованих до 16 кГц моно. Пост 2 описує набір даних: 973 анотовані уривки з 15 сесій обсмаження, розбиття за рівнем запису для запобігання витоку даних, навчання з ваговим регулюванням проти дисбалансу 20/80. Пост 3 описує навчання: дві спроби гіперпараметрів, коливання втрати через занадто агресивний темп навчання для 587 зразків та редизайн анотацій, що підняв точність з 87.5% до 100%.

Найбільш релевантні для цього поста числа — історичні базові baseline_v2 з PR-уedge-validation:

  • 86M параметрів, донавчений з MIT/ast-finetuned-audioset-10-10-0.4593, попередньо натренований на 2M кліпів AudioSet
  • 97.4% точність на тесті / 100% прецизність на baseline_v2 (191-зразковий тестовий набір, ізоляція на рівні записів)
  • ONNX INT8 на Mac: 96.86% точність, 1 додатковий FP. Це повна вартість якості від INT8 квантизації на тому ж тестовому наборі.
  • Модель та шлях розгортання ONNX тепер доступні за адресою huggingface.co/syamaner/coffee-first-crack-detection

Робочий процес на Pi

Валідація на Pi для PR #23 слідувала простій розподілу праці. Моделі ONNX експортувалися на Mac і синхронізувалися на Pi. Оз SSH’нувся до пристрою з Warp-терміналу та керував бенчмарками й скриптами оцінки безпосередньо. Моя роль — читати результати та приймати рішення щодо апаратного забезпечення.

Це mattered, коли Pi почала відмовлятися. SSH-сеанс, вихід бенчмарку, результат vcgencmd та наступна команда — усі в одному потоці терміналу. Не було окремого ноутбука для дебагу чи історії shell. Збої та наступні експерименти були в одному й тому ж місці.

Два скрипти керували всім на Pi:

  • scripts/evaluate_onnx.py: обробляє тестовий набір через модель ONNX і повертає точність, F1, матрицю плутаності та затримку на кожному зразку. Розроблений без залежності від PyTorch inference, щоб працювати на Pi без встановленого GPU torch.
  • scripts/benchmark_onnx_pi.py: 30 зафіксованих запусків після 5 розігрівів, використовуючи тестовий звук-заглушку, щоб ізолювати затримку інференсу від варіацій введення файлу.

Один нюанс під час налаштування: навіть попри те, що інференс йде повністю через ONNX Runtime, потрібна й PyTorch CPU на Pi для обчислення мел-фільтру ASTFeatureExtractor. Інсталювання розділене навмисно. requirements-pi.txt відповідає за ONNX та залежності аудіо, потім додано torch CPU:

pip install -r requirements-pi.txt
pip install torch --index-url https://download.pytorch.org/whl/cpu

torchaudio та optimum явно виключені з установки на Pi. ONNX Runtime обробляє все, починаючи з виходу з фільтрбанку.

Ось сесія оцінювання: Оз запускає evaluate_onnx.py через SSH на Pi, 4 потоки, читає числа затримки по мірі надходження:

PR #23 зібрав 36 коментарів Copilot під 6 раундами огляду, переважно щодо типових позначень типів, відсутності обробки помилок і перевірки крайових випадків. Корисна помилка — ціль бенчмарк-скрипта <500мс. Ця ціль була логічна на Mac, але на Pi завжди друкувала FAIL. Реальне питання полягало не в тому, чи модель впаде нижче 500 мс, а в тому, чи детектор зможе встигати за обсмаженням. Пр Production Pi-профіль пізніше відповідає, розширивши Hop до 7 секунд.

ONNX Export

Навчений контрпойнт — стандартна директорія Hugging Face save_pretrained: ваги моделі, конфігурація та препроцесор ASTFeatureExtractor. Доведення до Pi — це двокроковий процес: експортувати граф PyTorch у статичний FP32 ONNX-граф, потім стискати ваги до INT8. Оз виконав обидва кроки, запустивши навичку /export-onnx, яка показана у Пост 1, що виконує експорт, квантизацію та локальний бенчмарк послідовно, перш ніж позначити крок як завершений.

Крок перший використовує Hugging Face Optimum:

# src/coffee_first_crack/export_onnx.py
from optimum.onnxruntime import ORTModelForAudioClassification

ort_model = ORTModelForAudioClassification.from_pretrained(
    model_dir,
    export=True,
)
ort_model.save_pretrained(str(fp32_dir))

export=True запускає ONNX-трейсінг під час завантаження. Результат — exports/onnx/fp32/model.onnx обсягом 345МБ. Цей файл виконував 9.4 секунди на вікні на Pi при 1 потоці, зафіксовано у локальному артефакті валідації results/pi5_fp32_eval.json. Придатний для офлайн пакетної обробки, але не для живого процесу обсмаження.

Крок другий використовує onnxruntime.quantization.quantize_dynamic:

# src/coffee_first_crack/export_onnx.py
from onnxruntime.quantization import QuantType, quantize_dynamic

quantize_dynamic(
    model_input=str(fp32_path),
    model_output=str(int8_path),
    weight_type=QuantType.QInt8,
)

Динамічна квантізація переводить ваги в INT8 під час експорту; активації залишаються FP32 під час виконання. Я використав її замість статичної квантизації, бо статична вимагала б калібрувального набору даних для квантування також активів. Простіша динамічна підхід має менший вплив на якість. Такий же model_quantized.onnx з еталоном у 636мс на Apple Silicon лишається без змін на Pi з ARM Cortex-A76. Без перекомпіляції, без платфором-специфічних проходів.

Вихід одного експорту:

  • exports/onnx/fp32/model.onnx: 345MB, FP32
  • exports/onnx/int8/model_quantized.onnx: 89.9MB, ваги INT8

Це зменшення розміру у 3.84 рази. Повна порівняльна таблиця якості та затримки across платформи — у наступному розділі.

Налаштування препроцесора зберігається окремо у кожному підкаталозі. Кожна підпапка є самодостатньою. Можна скопіювати лише exports/onnx/int8/ на Pi і запускати інференс без повної структури репозиторію або пошуку конфігурації на рівні батьківської папки.

Існує другий шлях розгортання, який обходить ручне копіювання. Варіанти ONNX були завантажені на HF Hub разом із контрольною точкою PyTorch. syamaner/coffee-first-crack-detection містить підпапку onnx/int8/. Продукційний модуль інференсу, inference_onnx.py, за задумом HF Hub-перший: _DEFAULT_REPO_ID = "syamaner/coffee-first-crack-detection" та _DEFAULT_SUBFOLDER = "onnx/int8" — це зашиті за замовчуванням. При запуску він викликає hf_hub_download і кешує локально. За Pi з лише requirements-pi.txt та встановленням CPU-тensorflow не потрібно репозитарне клонування або передавання файлів. Валідція PR #23 використовувала локальні експорти, синхронізовані через scp, бо дані тест-розбиття теж були локальними. Нове розгортання продукції завантажує все з Хабу.

Числа платформи

Оцінювання v2 Pi5, зафіксоване локально як results/v2_pi5_int8_4t_eval.json, безпосередньо відповіло на питання про портативність: чи INT8 динамічна квантізація дає іншу точність на ARM64 проти Apple Silicon? Відповідь — ні. Модель Pi5 INT8 з 4 потоками, на тому ж тестовому наборі v2 з 191 зразком, повертає 96.86% точність, 96.9% прецизність та 1 хибнопозитивний. Це та сама матриця плутаності, що й на Mac INT8: [[154, 1], [5, 31]].

Модель Платформа Тестовий набір Точність Прецизність (FC) FP Затримка p50
PyTorch baseline_v2 Mac (MPS) v2, 191 зразків 97.4% 100% 0 n/a
ONNX INT8 Mac v2, 191 зразків 96.86% 96.9% 1 636мс
ONNX INT8 Pi5, 4 потоки v2, 191 зразків 96.86% 96.9% 1 2 092мс
ONNX FP32 Pi5, 1 потік v1, 45 зразків† 93.3% 91.3% 2 9 412мс
ONNX INT8 Pi5, 2 потоки v1, 45 зразків† 93.3% 91.3% 2 2 436мс

† тестовий набір v1 (6 обжарок, оригінальні анотації). Очікування щодо v1 — оцінки на 2 потоках у процесі ще не відбулися.

Загальна вартість INT8 квантизації, виміряна проти baseline v2 на тому ж тестовому наборі, становить 0.54% точності та 1 додатковий хибнопозитивний. Ця вартість однакова на Mac та Pi5. Ідентична матриця плутаності підтверює, що артефакт ONNX повністю портативний: однакові ваги та однакові передбачення на Apple Silicon та ARM Cortex-A76.

Рядки v1 існують для історії FP32 vs INT8 затримки: 9.4 секунди за FP32 повну, 2.4 секунди за 2 потоки INT8, 2.07 секунди за 4 потоки. Дані точності у цих рядках відповідають старому, меншому тестовому набору і не повинні сприйматися як платформа-кількісні цифри.

Період скошування порогу

Типовий поріг класифікації за замовчуванням — 0.5. Будь-яке вікно, у якому модель видає P(first_crack) >= 0.5, помічене. Для ізольованого класифікатора ця установка підійде. Для обсмажувального помічника наслідки асиметричні.

Хибнопозитивне чи хибнопозитивне означає, що система може неправильно вказати на появу перших тріщин через фоновий шум або удар барабана. Це може ініціювати автоматичну дію (таймер, реле вентилятора, оповіщення) не в той момент під час обсмаження. Торгівля не симетрична, тому поріг має відображати це.

Скошування пройшло від 0.50 до 0.95 на моделі Pi5 INT8, зафіксовано локально як results/pi5_threshold_sweep.json на тестовому наборі v1:

Порог Прецизність Відгук (FC) F1 FP FN
0.50 до 0.65 91.3% 95.5% 93.3% 2 1
0.70 до 0.75 90.9% 90.9% 90.9% 2 2
0.80 до 0.90 95.2% 90.9% 93.0% 1 2
0.95 100% 77.3% 87.2% 0 5

Числа між 0.50 і 0.65 ідентичні. Вірогідності на виході моделі далекий від порогу ухвалення рішення в більшості випадків. При 0.80 видаляється 1 FP. Решта FP, фрагмент no_first_crack зі значенням 0.941, зберігається аж до 0.90. Лише при 0.95 він зникає, знижуючи FP до нуля за рахунок зниження recall до 77.3% (5 додаткових пропущених вікон).

Розгорнутий профіль Pi не використовує 0.95. Він використує 0.90, у парі з шаром підтвердження. Логіка у configs/default.yaml:

# configs/default.yaml
pi_inference:
  window_size: 10.0
  overlap: 0.3           # 30% -> 7s hop, comfortable margin for 2-thread latency
  threshold: 0.90        # precision=0.952, recall=0.909, F1=0.930
  min_pops: 3            # 3 позитивних вікна потрібні в рамках підтвердження
  confirmation_window: 30.0  # секунд
  onnx_threads: 2        # залишає 2 ядра вільними для сервера MCP + інтерфейс агента

Підводна оцінка: одне вікно з балами вище 0.90 не запускає детектор. Три позитивних вікна протягом 30-секундного періоду мають узгодитися. Ізольований хибнопозитив від фонових шумів не може вижити при вимаганні підтвердження. Для того, щоб хибнопозитив запустив систему, три окремі аудіо-вікна, з інтервалом 7 секунд, повинні незалежно набрати понад 0.90. Я не бачив такого патерну в тестовому наборі. Вибраний поріг 0.941 з’являється лише раз, в одному уривку, а не у трьох послідовних вікнах живого обсмаження.

Два додаткових зауваги щодо Pi-профілю. По-перше, перекриття знижується з 70% (за замовчуванням, hop 3 секунди) до 30% (hop 7 секунд). З інференсом на 2 потоки на 2.45 секунди за вікно, hop 3 секунди майже не залишає вільного часу і ризикує відставати від аудіопотоку. Ширший hop дає 4.5 секунди запасу на цикл. По-друге, кількість потоків становить 2, а не 4, бо Pi запускає не лише детектор. Сервер MCP та UI-агент використовують той же пристрій; обмеження ONNX до 2 ядер запобігає «голоду» решти стеку під час інференсу.

Історія з апаратним забезпеченням

Перший тривалий запуск інференсу на Pi використав зарядний пристрій Apple 96W USB-C. Він був на столі. Бенчмарк працював на 1 потоці без проблем. На 2 потоках Pi5 обірвався посеред запуску.

Oz уже був у SSH-сесії. Наступна команда була vcgencmd get_throttled:

$ vcgencmd get_throttled
throttled=0x50000

0x50000 вказує на біт 16 (виявлено зниження напруги) та пригальмовування. Pi5 витрачав більше потужності, ніж міг забезпечити зарядний пристрій, а прошивка зменшувала такти для компенсації. При серйозному дефіциті платформа зупиняється.

Заряд Apple 96W negotiate 5V при 3A на USB-C, тобто 15 ват. Raspberry Pi 5 за стабільного навантаження 2+ потоки інференсу споживає до 5V/5A, що 27 ват. Це в специфікаціях обладнання RPi5. Профіль живлення 5V/5A USB Power Delivery, який потрібен Pi, не узгоджується з більшістю ноутбучних зарядок, включаючи Apple 96W. Іменування вводить в оману: “96W” відноситься до високовольтного профілю MacBook, а не до лінії 5V.

Oz одразу зробив обхід: зменшив onnx_threads до 1 і повторно запустив. Однопоточна інференсія залишалася в рамках бюджету 15W, тести завершилися. Але 1-поточна інференсія за 9.4 секунди на вікно — це вже не ціль розгортання. Потрібна була апаратна заміна, а не конфігурація.

Ґеміні було залучено для зіставлення документації регістра throttle RPi5 та офіційної специфікації PSU. Людина ухвалила рішення: офіційний Raspberry Pi 27W USB-C Power Supply (5V/5A) — потрібний апгрейд, а не зміна конфігурації.

Ось сесія налагодження SSH: Оз читає vcgencmd get_throttled, діагностує прапор 0x50000 та коригує потоки як проміжний варіант до прийняття рішення про апаратне забезпечення:

Oz SSH'd into the RPi5, running vcgencmd get_throttled and diagnosing the 0x50000 under-voltage flag

Після заміни блоку живлення, 4-потокове інференсійне виконання без збоїв. Але зламався другий прапор під тривким навантаженням:

$ vcgencmd get_throttled
throttled=0xe0000

Oz SSH'd into the RPi5, after PSU swap + cooling

0xe0000 устанавливає біти 17, 18 та 19: частота ARM обмежена, доведено, що стався throttling, активна м’яка межа температури. Ядро CPU досягло 77°C під тривалим інференсом. У Pi5 немає теплоотводу за замовчуванням; тривалий інференс трансформера є тепловим тестом, який пасивна конструкція не може пройти.

Офіційний активний кулер Raspberry Pi довів робочу температуру до 45°C при тривалому 4-потоковому інференсі, зниження температури на 32°C. При 45°C vcgencmd get_throttled повертає 0x0. Стабильно.

Останні вимоги до апаратного забезпечення з цієї відлагоджувальної сесії зафіксовані в AGENTS.md:

### Вимоги до апаратного забезпечення RPi5
- Призначення живлення: офіційний RPi5 27W (5V/5A) USB-C. Звичайні зарядні пристрої (5V/3A, включно з Apple 96W) призводять до збоїв через перенавантаження по напрузі.
- Охолодження: обов'язковий активний кулер. Підтримувані навантаження інференсу на 77°C+ без нього.
- Потоки: за замовчуванням 2 через прапор `--threads`. 4 потоки потребують PSU 27W + активного кулера.

Увесь цикл відлагодження, включаючи SSH, вивід vcgencmd та подальше оновлення AGENTS.md, залишався у одному Warp-терміналі.

Висновок

Розгортання на виробничому Pi виконує inference_onnx.py --profile pi_inference: 2 потоки ONNX, 30% перекриття (7-секундний hop), поріг 0.90, min_pops: 3 підтвердження. За таких параметрів, за умови 27W PSU та активного кулера, затримка на вікно становить 2.45 секунди (p50). Це дає 4.5 секунди вільного часу між викликами інференсу за 7-секундним hop, що достатньо для того, щоб сервер MCP та інтерфейс агента одночасно працювали на одному пристрої.

Для цього випадку це достатньо. Перше тріщання — це не подія мілісекундної точності. Це тривала аудіофаза протягом 60–90 секунд. Детектор, що підтверджує появу протягом 30 секунд з трьома позитивними вікнами через 7 секунд, узгоджується з часом реального обсмаження. Результат 2.07 секунди за 4 потоки — це вищий поріг продуктивності з оптимальним апаратним забезпеченням; 2.45 секунди за 2 потоки — стабільна продукційна мета.

Ці числа також демонструють межу трансформерів на краю. Модель з 86M параметрів, яка потребує 9.4 секунди FP32 на недорогому ARM-побудові, не є універсальною архітектурою краю. Вона працює тут, бо інференс відбувається повільно, а подія триває досить довго, щоб її зафіксувати у кількох вікнах. Розгорніть ту ж модель на задачі з відгуком менше за секунду — вам потрібна набагато менша архітектура або спеціалізований інференс-апарат. INT8-квантизація допомогла: зменшення розміру на 3.84 рази, поліпшення затримки на 4.5 раза порівняно з FP32. Це не перетворює великий трансформер на модель класу мікроконтролера.

Пост 5 висвітлює іншу половину використання моделі: як зробити її доступною людям, які не мають SSH-сесій до Raspberry Pi. Публікація карти моделі та карти набору даних, створення Gradio Space, що працює у контейнері, та чотири помилки платформ, включно з SSR/asyncio-крашем на Python 3.13, кожна з яких виправлялася шляхом вставлення логів HF Space Оз та отримання виправлень за секунди.

Посилання

Проєкт:

Інструменти:

Посилання на джерела

1. ONNX Runtime та квантізація

  • ONNX Runtime Dynamic Quantisation: Документує quantize_dynamic з QuantType.QInt8, підхід, який використовується для експорту INT8. Пояснює різницю між динамічною (ваги лише) та статичною (потрібна калібраційна вибірка) квантизацією.
  • Hugging Face Optimum ONNX Export: ORTModelForAudioClassification.from_pretrained(export=True) активує ONNX-трейсінг, який використовується в export_onnx.py.

2. Апаратне забезпечення Raspberry Pi 5

  • Raspberry Pi 5 Product Brief (PDF): Вказує вимогу до 5V/5A (27W) USB-C PD для повного навантаження. Стандартні джерела живлення 5V/3A явно позначені як недостатні для тривалих завдань.
  • vcgencmd Documentation (Raspberry Pi): Документи get_throttled та бітові прапори: біт 16 (виявлено зниження напруги), біт 17 (ARM frequency capped), біт 18 (throttled), біт 19 (soft temp limit).

3. Розгортання на краю та оптимізація моделей

  • Hugging Face Audio Classification Fine-Tuning Guide: Контекст архітектури моделі AST та вимоги до препроцесингу ASTFeatureExtractor, які переноситься до розгортання на краю.

April 29, 2026 at 08:26PM