Потоковый ASR на ARM: ExecuTorch ускорил его в 2,1 раза

Потоковый ASR на ARM: ExecuTorch ускорил его в 2,1 раза

Когда вы говорите с телефоном, текст появляется на экране по мере речи. Звучит как мелочь, но именно из-за неё распознавание речи на устройстве устроено совсем не так, как расшифровка готового файла. В пакетном режиме модель видит запись целиком и позволяет себе смотреть вперёд и назад по контексту. В потоковом она обязана выдавать слова, ещё не зная, что будет сказано через секунду.

Команда PyTorch показала, как такая модель живёт на ARM-ядре обычного смартфона. Взяли мультиязычную Nemotron 3.5 от NVIDIA, провели её через ExecuTorch, замерили точность на четырёх датасетах и время работы на Google Pixel 11. Получился редкий случай, когда в одном тексте есть и методика, и цифры, и честный список того, что ещё не оптимизировано.

Что такое потоковый ASR

Потоковый ASR (streaming automatic speech recognition) это распознавание речи порциями аудио фиксированного размера, чанками, при котором модель выдаёт текст с задержкой в доли секунды. Модель не знает будущего: она видит только текущий чанк плюс небольшой запас контекста, называемый lookahead, и обязана решать на лету. Именно поэтому качество потоковой расшифровки всегда ниже пакетной, и именно поэтому её нельзя оценивать теми же метриками.

От модели PyTorch до инструкций процессора

ExecuTorch это фреймворк инференса на периферийных устройствах с инструментами экспорта и лёгким рантаймом. Экспорт готовит модель и складывает её в файл .pte, который загружает runtime на устройстве. Поддерживаемые операции уходят в XNNPACK, библиотеку инференса для CPU. На Android XNNPACK умеет выбирать микрокернели из Arm KleidiAI: небольшие оптимизированные подпрограммы, которые считают внутреннюю часть матричных операций.

Дальше в дело вступает SME2, Scalable Matrix Extension 2, расширение системы команд Arm для матричных вычислений, включая GEMM. В реализации Arm C1 матричный блок с SME2 общий для ядер кластера, и это важно для понимания замеров: ускорение достаётся не одному потоку, а всем, кто делит этот блок.

Что содержит один потоковый вызов

Nemotron 3.5 построена на архитектуре RNN-T из трёх частей. Энкодер на слоях FastConformer превращает аудио в представление, предиктор на LSTM держит историю уже выданных токенов, а джойнер, маленький полносвязный слой, смешивает выходы двух предыдущих и предсказывает следующий токен. Он же умеет выдавать blank, пустой символ, который просто сдвигает декодирование вперёд.

Фронтенд считает log-mel признаки: один вектор на каждые 10 мс звука. Энкодер прореживает временную ось в 8 раз, то есть одна его позиция соответствует 80 мс аудио. Lookahead задаёт, сколько будущего контекста доступно внутри чанка, а частота чанков определяет, как часто приходит свежее аудио. При lookahead 13 вызов собирает 1 120 мс аудио и выдаёт 14 позиций энкодера. Значения 0, 3 и 6 дают чанки 80, 320 и 560 мс соответственно. Чем длиннее чанк, тем позже появляется первый текст и тем реже обновляется строка на экране.

Точность зависит от размера чанка

Точность мерили на хосте через рантайм ExecuTorch с бэкендом XNNPACK, а локальный адаптер потокового режима использовал harness из Hugging Face Open ASR Leaderboard. Проверили три точности (FP32, FP16, INT8) на четырёх размерах чанка. В режиме INT8 применяли схему QC8W: динамические INT8-активации и per-channel INT8-веса для подходящих линейных операций, выход при этом остаётся FP32.

Каждая точка на графике WER покрывает 14 019 высказываний из четырёх корпусов: LibriSpeech Other, AMI Cleaned, Earnings22 и VoxPopuli Cleaned. Результат ожидаемый, но приятный: длинные чанки заметно улучшают итоговый транскрипт. На отрезке от 80 до 1 120 мс WER для FP16 падает с 15,2% до 11,5%, для QC8W с 15,5% до 11,7%. FP16 при этом практически совпадает с FP32, разница меньше 0,03 процентного пункта на всех размерах чанка.

Вот тут и спрятана ловушка. WER на хосте говорит, насколько правильный текст получится, но ничего не говорит о том, как быстро он появится на экране. А для потокового распознавания это половина продукта.

Реальный бюджет времени на телефоне

Второй замер сделали на Google Pixel 11. Обработали запись длиной 24,9 секунды, чанками по 1 120 мс, одним потоком инференса, привязанным к среднему ядру CPU: именно такое ядро отвечает за устойчивую фоновую нагрузку в реальном приложении. На каждую конфигурацию два прогревочных прогона и один измеренный, общее время считается от предобработки аудио до финальной детокенизации.

С включённым SME2 картина меняется сильно. FP16 проходит запись за 4,61 секунды вместо 9,79, то есть в 2,12 раза быстрее. QC8W ускоряется с 5,20 до 3,24 секунды, в 1,61 раза. Если перевести это в real-time factor, отношение времени обработки к длительности аудио, получается 0,19 для FP16 и 0,13 для QC8W. Оба варианта обрабатывают запись в пять-семь раз быстрее, чем она звучит, и это тот запас, который можно отдать другим задачам приложения.

Где именно ушло время

ExecuTorch умеет снимать ETDump, журнал событий и таймингов операторов. Если сгруппировать операции по категориям, видно структуру нагрузки: матричные умножения (GEMM) в плотных слоях энкодера, перемещение данных (копирования, конкатенации, повторы, поиск в эмбеддингах), поэлементные операции, свёртки с нормализациями, прочая мелочь и накладные расходы рантайма.

С SME2 время GEMM падает с 8,2 до 3,1 секунды для FP16 и с 4,1 до 2,0 секунды для QC8W. Доля GEMM в общем времени при этом снижается с 84% до 67% и с 78% до 62%. А перемещение данных занимает примерно одно и то же время в обеих сборках, поэтому его относительная доля растёт: с 8% до 17% для FP16 и с 9% до 17% для QC8W. Проще говоря, матрицы ускорили, и теперь узким местом становится перекладывание тензоров. Это готовый адрес для следующей оптимизации, и он лежит уже не в арифметике.

Какие ядра действительно работали

ETDump показывает, какие семейства операторов дорогие, но не называет нативные микрокернели. Для этого взяли Arm Streamline: он сэмплирует нативные вызовы и снимает аппаратные счётчики. На Pixel 11 утилизация матричного блока SME составила 38,4% для FP16 и 53,7% для QC8W. Это доля времени, когда матричное железо было занято, а не процент от пиковой арифметической производительности.

Имена функций в сэмплах говорят сами за себя. Для FP16 со SME2 вызывался kai_kernel_matmul_clamp_f16_f16p2vlx2_f16p2vlx2_2vlx2vl_sme2_mopa из KleidiAI, то есть матричное накопление внешнего произведения на SME2. Для QC8W работали два ядра KleidiAI: kai_kernel_matmul_clamp_f32_qai8dxp1vlx4_qsi8cxp4vlx4_1vlx4vl_sme2_mopa для INT8-входов с выходом FP32 и отдельное FP32-ядро SME2. В сборках без SME2 картина другая: FP16 уходил на NEON-ядро XNNPACK xnn_f16_gemm_minmax_ukernel_6x16__asm_aarch64_neonfp16arith_cortex_a75, а INT8 использовал NEON I8MM через kai_run_matmul_clamp_f32_qai8dxp4x8_qsi8cxp4x8_16x4_neon_i8mm.

Сэмплы вскрывают и неидеальность. Даже в сборке со SME2 часть GEMM в FP16 всё ещё шла через NEON, а часть QC8W-умножений выполнялась в FP32. То есть покрытие микрокернелями неполное, и это прямой резерв скорости. Отдельно авторы отмечают пробел в инструментах: ETDump знает про операторы модели, Streamline знает про нативные функции, но мостика, который связал бы одно с другим, в экосистеме нет. Сопоставление слоёв модели с наблюдаемыми ядрами пока остаётся ручной работой.

Как выбрать рабочую точку

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

Если задать потолок в 13% WER, обе точности проходят на чанках 320, 560 и 1 120 мс. Чанк 320 мс даёт самый быстрый отклик при 12,67% WER у FP16 и 12,97% у QC8W. Чанк 1 120 мс вытягивает качество до 11,50% и 11,72%, но требует больше аудио до появления первых слов.

Если задать бюджет задержки в 800 мс, в него попадают конфигурации на 320 и 560 мс, и обе укладываются под потолок 13% WER. На 560 мс FP16 даёт 12,01% WER при оценке задержки 691 мс, а QC8W 12,26% при 660 мс. Конфигурации на 80 мс проваливают требование по качеству, конфигурации на 1 120 мс не влезают в бюджет по времени. Оценка задержки до первого токена складывалась из времени сбора аудио и медианного времени последовательности энкодер-предиктор-джойнер, по 75 замеров на конфигурацию в три раунда, на синтетическом входе и со свежим состоянием потока.

Что это значит на практике

Первое и главное: потоковую модель надо мерить на том размере чанка, с которым она будет работать в продукте. Точность на полной записи не предсказывает качество потока, и разница между 15,2% и 11,5% WER это не погрешность, а разница между «переспрашивает через слово» и «работает».

Второе: точность и размер чанка нельзя выбирать по отдельности. FP16 и QC8W на графике идут почти параллельно, и на SME2 разрыв в скорости между ними сокращается: FP16 получает от матричного расширения больше (2,12 раза против 1,61), поэтому выбор «INT8 ради скорости» перестаёт быть автоматическим. Если память и тепловой пакет позволяют FP16, разумно начинать с него и опускаться в INT8 только там, где реально не хватает бюджета.

Третье: выигрыш от SME2 не бесплатный по определению, он зависит от того, попали ли горячие операции на ускоренные ядра. Часть GEMM осталась на NEON, часть INT8-считалок ушла в FP32, и утилизация матричного блока составила 38,4% и 53,7%, а не сто процентов. Значит, следующий прирост даст не новое железо, а полнота покрытия микрокернелями и работа с перемещением данных, доля которого уже выросла до 17%.

Для продукта это открывает вполне бытовые сценарии. Локальная расшифровка звонков и диктовки не отправляет аудио на сервер, работает без сети и не стоит денег за минуту, а на Pixel 11 обработка идёт в 5–7 раз быстрее реального времени. Ограничением остаются память, нагрев и качество микрофона, но не арифметика.

Часто задаваемые вопросы

Чем потоковый ASR отличается от пакетного?

Пакетный режим получает всю запись и может смотреть вперёд и назад по контексту, поэтому даёт минимальный WER. Потоковый работает чанками и видит только прошлое плюс небольшой lookahead, зато выдаёт текст во время речи. Отсюда разница в цифрах: 11,5% против заметно меньших значений у пакетных моделей.

Что реально даёт SME2 на телефоне?

На Pixel 11 матричное расширение ускорило обработку записи в 2,12 раза для FP16 и в 1,61 раза для QC8W. Время GEMM упало с 8,2 до 3,1 секунды и с 4,1 до 2,0 секунды соответственно. Real-time factor после этого составил 0,19 и 0,13, то есть запас по времени в пять-семь раз.

Можно ли перейти на INT8 без потери качества?

Почти. Схема QC8W с динамическими активациями и per-channel весами отстаёт от FP16 всего на 0,2 процентного пункта WER: 11,7% против 11,5% на чанке 1 120 мс. Но выигрыш по времени на SME2 меньше, чем у FP16, поэтому INT8 имеет смысл прежде всего там, где ограничена память или нагрев.

Итог

Потоковое распознавание речи на устройстве перестало быть компромиссом ради приватности. ExecuTorch доводит модель PyTorch до микрокернелей KleidiAI, SME2 снимает с матричных операций больше половины времени, а аккуратный выбор чанка и точности даёт WER около 12% при задержке в пределах 700 мс. Осталось дожать перемещение данных и покрытие ядрами, и это уже задача для тех, кто пишет kernels, а не для тех, кто выбирает модель.

Если вы делаете голосовое приложение, начните с замера на своём железе и своём размере чанка. Цифры из этого разбора годятся как ориентир, но не как гарантия: у другого CPU будет другой баланс между NEON, I8MM и SME2.

← Все записи