Начал неспеша затаскивать код на GCC ARM.
Первый блин комом: отсутствуют std::call_once и std::once_flag, которые должны быть в хедере <mutex>.
Учитывая, что среда однозадачна, сделал такую заглушку(stub):
#include <tuple> #include <bits/invoke.h> namespace std { struct once_flag { constexpr once_flag() noexcept = default; once_flag( const once_flag&) = delete; once_flag& operator=( const once_flag&) = delete; }; template<typename _Callable, typename... _Args> void call_once( once_flag& __once, _Callable&& __f, _Args&&... __args) { auto __callable = [&] { std::__invoke( std::forward<_Callable>( __f), std::forward<_Args>( __args)...); }; } }
Проверил её на PC/MingW, отредактировав штатный <mutex> в mingw и пересобрал onnxruntime : вариант рабочий.
Данную хрень Микрософт любит пихать в Лямбда-функторы:
// Returns rprog_, computing it if needed. re2::Prog* RE2::ReverseProg() const { std::call_once( rprog_once_, []( const RE2* re) { re->rprog_ = re->suffix_regexp_->CompileToReverseProg( re->options_.max_mem( ) / 3); if ( re->rprog_ == NULL) { if ( re->options_.log_errors( )) LOG( ERROR) << "Error reverse compiling '" << trunc( *re->pattern_) << "'"; } }, this); return rprog_; }
По началу делал так - тоже работало:
// Returns rprog_, computing it if needed. re2::Prog* RE2::ReverseProg() const { const RE2* re; re->rprog_ = re->suffix_regexp_->CompileToReverseProg( re->options_.max_mem( ) / 3); if ( re->rprog_ == NULL) { if ( re->options_.log_errors( )) LOG( ERROR) << "Error reverse compiling '" << trunc( *re->pattern_) << "'"; } return rprog_; }
Но таких мест много, поэтому было решено сделать через вышеуказанную заглушку, чтобы не исправлять в разных местах в сотнях сорцов.
Едем дальше...
Ещё один: std::condition_variable в минимальном исполнении:
namespace std { class condition_variable { public: condition_variable() noexcept; ~condition_variable( ) noexcept; condition_variable( const condition_variable&) = delete; condition_variable& operator=( const condition_variable&) = delete; void notify_one( ) noexcept; void notify_all( ) noexcept; void wait( unique_lock<mutex>& __lock) noexcept; }; } // namespace
Запустил ONNXruntime на T113-s3 (ARM Cortex-A7). Пока прогнал тестовый пример - простая нейросеть:
Результаты совпадают: на ПК и T113-s3 - одинаковые значения.
Для приличия нужно дописать нижний уровень файлового ввода-вывода, чтобы std::fstream работал как минимум на чтение файлов с SD-карты.
Несколько слов о том, как портировал с Винды/ПК на голое железо:

На каждом этапе делается что-то одно. Крайне тяжело сделать всё сразу - сразу этап 5, поскольку требуется изменить множество параметров и постоянно чистить код.
Также требовалась трассировка сборки: нужно было определить какие файлы исходного кода нужно скомпилировать и правильные параметры сборки + глобальные дефайны, отвечающие за условную компиляцию.
Выполненные преобразования:
1) Динамическая библиотека (DLL) => Статическая библиотека
2) Visual Studio => BAT-скрипты
3) MSVC => GCC
4) ПК => ARM
На каждом этапе делается периодическая проверка обратной связи: при каждой проверке все исходные коды ДОЛЖНЫ предоставлять работоспособную среду для программного проекта (например, кодек). Если что-то сломалось, возврат к предыдущему этапу и исправляем.
Gradius
Несколько слов о том, как портировал с Винды/ПК на голое железо:
...
Впечатляет!
Я имею в виду не только этот пост.
Если уж это не учебник, то к прочтению точно должно быть рекомендовано.
Сложная модель тоже даёт одинаковые результаты:

1)
2)
3)
Сделал замеры по потреблению памяти нейросетками во время загрузки и выполнения:
Модель WavLM: размер файла - 338 МБ, расходуемая память загрузки в onnxruntime - 647,5 МБ Модель Focal Compressor: размер файла - 75,8 МБ, расходуемая память загрузки в onnxruntime - 138,8 МБ Модель Focal Decompressor: размер файла - 75,8 МБ, расходуемая память загрузки в onnxruntime - 151,1 МБ Модель Vocos: размер файла - 65 МБ, расходуемая память загрузки в onnxruntime - 119,9 МБ Начальный инит onnxruntime: 3,1 МБ (без сеток) Дополнительная расходы на память во время инференса: 8,2 МБ.
Что я вижу: загрузка каждой модели требует почти в 2 раза больше памяти. Делаю предположение - возможно, в onxruntime не освобождается память после загрузки файла модели: модель парсится в другой участок памяти, а исходный образ файла модели остаётся в памяти.
Попробую исследовать.
Gradius
не освобождается память после загрузки файла модели: модель парсится в другой участок памяти, а исходный образ файла модели остаётся в памяти.
Да, похоже.
Gradius
> Делаю предположение - возможно, в onxruntime не освобождается память после загрузки файла модели: модель парсится в другой участок памяти, а исходный образ файла модели остаётся в памяти.
Анализ кода подсказал мне два полезных сета, чтобы убрать перерасход памяти:
Ort::SessionOptions session_options; session_options.AddConfigEntry("session.use_ort_model_bytes_directly","1"); session_options.AddConfigEntry( "session.disable_prepacking","1");
Теперь все 4 модели + среда выполнения требуют 628 МБ. А было 1069 МБ.
Но чёрт возьми, даже вокодер Vocos не удаётся загрузить в память T113-s3 :)))
Его файл модели 65 МБ, значит пиковый расход - примерно в 2 раза больше, тоесть 130 МБ. У T113-s3 всего 128 МБ, из них 10 МБ сама программа и 1 МБ настройка MMU. Итого свободно 117 МБ, что не хватает прогрузить и пропарсить Vocos.
Следующий шаг: квантизация моделей и/или переход на доску с бОльшим объёмом памяти. Плюс можно добавить сюда дистилляцию модели WavLM (338 МБ) на более легковесную. Ну и плюс может можно выполнить оптимизацию модели (на стороне ПК средствами ONNXRuntime).
P.S. Ещё можно попробовать парсить с файла, не загружая в буфер. Но это будет медленно скорее всего. Да и не сильно просто это сделать.
Gradius
> Следующий шаг: квантизация моделей
Попробовал этот шаг. Все модели сконвертились, кроме кодера WavLM - почему-то не захотел.
Про конвертацию моделей в fp16 здесь:
https://onnxruntime.ai/docs/performance/model-optimizations/float16.html
Размер модели уменьшается почти в 2 раза. Но не всё так просто:
1) Размерность входного и выходного тензоров - специальный тип. Операторы и типкасты расписаны здесь:
https://onnxruntime.ai/docs/api/c/struct_ort_1_1_float16__t.html
Но можно не заморачиваться и заставить модель сохранить входной и выходной тензоры в традиционном формате fp32 (keep_io_types=True):
model=onnx.load("coder.onnx") model_fp16=float16.convert_float_to_float16( model,keep_io_types=True) onnx.save( model_fp16,"coder_fp16.onnx")
2) При многократном выполнении сетки, первые 2 итерации - растёт потребление памяти, а потом останавливается.
Лечится принудительной установкой следующих сетов:
session_options.DisableMemPattern(); session_options.DisableCpuMemArena( ); session_options.AddConfigEntry( "session.use_memory_arena","0"); session_options.AddConfigEntry( "session.use_arena_allocation","0");
Подробнее тут:
https://github.com/microsoft/onnxruntime/discussions/22763
3) Скорость работы сконверченной модели fp16 на целевом железе T113-s3 немного просела.
И кстати, видна небольшая разница в результатах выдачи на ПК и ARM: в младших разрядах.
Gradius
> Скорость работы сконверченной модели fp16 на целевом железе T113-s3 немного просела.
Не работает векторный asm или нет нативной поддержки fp16? Нейросети — практически идеальный случай для векторизации, должно быть, наоборот, ускорение в ~2 раза.
}:+()___ [Smile]
> Не работает векторный asm или нет нативной поддержки fp16? Нейросети — практически идеальный случай для векторизации, должно быть, наоборот, ускорение в ~2 раза.
Скорее всего нет нативной поддержки fp16. Есть только fp32, fp64.
NEON включен. Часть сорцов даже с ним на ассемблере.
Заданы ключи:
-mfloat-abi=hard -mfpu=neon-vfpv4 -ftree-vectorize -fno-math-errno
Замерил время: у fp32 модели 1 выполнение 330 мс, у fp16 модели 406 мс.
Плюс ещё обнаружил, что если модель fp16 имеет сильно большие модули значений, то они будут nan и inf. Такое у Focal Compressor.
Там потом идёт нормализация вектора перед квантователем - но это не слой, а жёсткая функция без параметров обучения.
P.S. Проверил. Действительно, аппаратная плавучка fp16 не поддерживается у T113-s3. Есть только аппаратная плавучка fp32, fp64 и NEON.
https://developer.arm.com/documentation/dui0774/g/chr1383660321827
printf("0x%02x\n", __ARM_FP); вернул 0x0e
Найдены оптимальные конфигурации моделей нейросетей для кодека, исходя из размера памяти и требуемого быстродействия. Благо, что мы можем это проверить на необученных сетях со случайными весами, многократно экспериментируя с параметрами.
Требование к памяти отходит на второй план, так как это уже следствие от более первичного требования - обеспечить нужное быстродействие. В итоге, модели уменьшились: остался запас по памяти даже.
Общее время работы кодека определяется формулой (выведена из практических результатов):
Tcodec = Tcoder + Tcompressor + 2 * (Tdecompressor + Tdecoder)
Множитель 2 на декодирующей стороне: декомпрессор и декодер работают дважды, так как требуется перекрытие с оконными функциями для сглаживания сегментов в один непрерывный поток. Выбрано перекрытие окон 50%.
Львиную долю времени отнимает декодер(Tdecoder), редуцированная версия вокодера Vocos.
При сегменте в 64 фрейма (по 20 мс) каждый: суммарное время Tcodec не должно превышать 1280 мс. Именно при таких условиях кодек будет успевать потоково кодировать и декодировать.
Кроме того, нужно ещё учесть время на затраты вычислений FFT.
Широкое пространство свободы:
- на практике при полу-дуплексной связи требуется только либо кодирование, либо декодирование
- можно использовать два ядра : одно кодирует, другое декодирует - тем самым увеличив модели, улучшить результат и получить бОльший временной ресурс Tcodec.
Прорабатываю схему дистилляции с пониженным размером выходного тензора у обучаемой сети:

Суть следующая:
Есть эталонный кодер - мощный обученный на нескольких сотнях тысячах часов трансформер WavLM, обладающий следующими ценными свойствами:
- подавляет шум
- подавляет помеху 50 Гц
- выделяет речевые признаки
И это лучше, чем просто использовать спектр на входе Focal Compressor.
Фичность WavLM имеет размерность 1024, сама модель большая.
Теперь необходимо сделать не только дистилляцию, но и понизить размерность фичности новой легковесной сети (назвал её MelLM).
Для этого необходимо добавить проекцию (Projection: линейный слой без активации), чтобы сравнять размерность выходных тензоров по фичам (с 256 до 1024).
Затем произвести обучение(MelLM и Projection) по критерию лосса MSE.
Использовать обученную сеть MelLM, естественно уже без слоя проекции: features mini размерности 256.
Gradius
Найдены оптимальные конфигурации моделей нейросетей для кодека, исходя из размера памяти и требуемого быстродействия.
Нужно ли проводить предворительные экспиременты, или сразу на "железе" будешь делать?
Gradius
Прорабатываю схему дистилляции с пониженным размером выходного тензора у обучаемой сети:
Значит придётся обучать сеть снова, или уже обученную использовать будешь?
Gradius
Есть эталонный кодер - мощный обученный на нескольких сотнях тысячах часов трансформер WavLM, обладающий следующими ценными свойствами:
Правильно я понимаю, что это уже готовые сторонние данные?
flint2
> Нужно ли проводить предворительные экспиременты, или сразу на "железе" будешь делать?
Замер времени выполенения моделей и их аппетиты к памяти делаю уже на целевом железе. Как я писал выше, сеть может быть необучена - с рэндомными весами. Обученность моделей не влияет на время исполенения (если конечно там нет условий внутри) и требование к памяти.
flint2
> Значит придётся обучать сеть снова, или уже обученную использовать будешь?
Естественно. Все 4 узла кодека: кодер, компрессор, декомпрессор и декодер обучаются по-новой. В противном случае придётся тащить за собой проекционные костыли для снижения/повышения размерности.
К примеру - переобучил вокодер Vocos. При этом уменьшил его модель с 65 до 9 МБ. Просто уменьшил число блоков ConvNET с 8 до 4, входной тензор 80 мел-полос, а не 1024. Плюс число каналов скрытых слоёв с 512 уменьшил до 160. Результат - работает. Правда, голос получился с небольшим эхом, но это из-за того что вместо 2,5 млн. итераций обучения сделал всего 500 000.
flint2
> Правильно я понимаю, что это уже готовые сторонние данные?
Верно. Трансформер-кодер использую уже обученным.
Как вы в одиночку представляете себе обучение на сотнях тысячах часов разговорной речи на одном GPU? :) При этом данные на выходе у завода-изготовителя (Microsoft) настолько насыщенные и репрезентативные, что трескаются, как переспелый и вкусный арбуз :)
https://github.com/microsoft/unilm/tree/master/wavlm
https://arxiv.org/pdf/2110.13900
А вот сдистиллировать знания, да и ещё уменьшить тензор - это будет намного быстрее. 68 000 итераций достаточно, дальше лосс уменьшается очень лениво.
Вот что получилось: слева - выход с учителя (WavLM, 360 MB, dim 1024, волна на входе), справа - выход ученика + проекция (MelLM, 6 МБ, dim 256, спектр на входе) :
Похожи?
P.S. По сути, я делаю свой кодек, в основе которого лежат такие же принципы, как в FocalCodec.
С некоторыми отличиями:
- поддержка потокового/каузального режима
- спектр на входе, вместо звуковой волны
- уменьшенные модели
Gradius
Как вы в одиночку представляете себе обучение на сотнях тысячах часов разговорной речи на одном GPU? :)
Примерно представляю. ))
Я с горяча похожее делал, но только с текстом - вся библиотека flibusta + по мелочам (СЕРИЯ.Звёзды_научной_фантастики, СЕРИЯ.Настоящая_фантастика, Библиотека приключений и научной фантастики...) в общем вся фантастика без повторений + детективы и триллеры.. Ну очень долго всё, месяцы.
Gradius
Похожи?
Очень.
Gradius
По сути, я делаю свой кодек, в основе которого лежат такие же принципы, как в FocalCodec.
С некоторыми отличиями:
Моя понимать.
Круто!