Привет, Хабр! Меня зовут Владимир Никулин, я технический лидер команды продуктивизации нейросетевых решений в MWS AI. Мы развиваем платформу синтеза и распознавания речи Audiogram, которая, в свою очередь, является частью еще более масштабной платформы для создания ИИ-агентов — MWS AI Agents Platform.
Часто нашим заказчикам нужно компактное коробочное решение, которое можно запустить на CPU при отсутствии GPU или для простой экономии ресурсов. В этом материале по следам своего же доклада на AiConf на примере нашего модуля автоматического распознавания речи (Automatic Speech Recognition или кратко — ASR) я расскажу:
как мы продуктивизировали модели на CPU, сохраняя качество (WER), сопоставимое с моделями, развернутыми в GPU-кластерах;
какие подходы для сравнения по производительности и качеству использовали, чтобы не попасть в ловушку усреднения метрик;
с какими неожиданностями мы столкнулись при смене версий Triton Inference Server и бэкендов (ONNX, OpenVINO).
Поехали!
Текста много. Так что вот оглавление:
Архитектура модуля ASR
Прежде чем приступить к основной теме разговора, дам немного информации о нашей архитектуре ASR. Она микросервисная: API и агенты (серверные клиенты) работают на CPU, а серверы с моделями для детекции активности речи (Voice Activity Detection или VAD) и собственно ASR традиционно требуют GPU. На рисунке 1 показал, как у нас устроено взаимодействие микросервисов.

Мы спроектировали архитектуру так, чтобы ее можно было легко расширять под бизнес-логику заказчика. Один из вариантов расширения, например, — добавление сервера пост-процессинга. Он может расставлять знаки препинания, приводить числа к удобному формату или исправлять регистр, превращая «сырой» поток слов в читаемый текст для отчета или диалога.
Важный нюанс: наша ASR умеет работать в двух принципиально разных режимах — онлайн и офлайн. Подход к оптимизации для каждого из них будет своим, так как приоритетные метрики разные.
Режим онлайн предназначен для диалоговых сценариев: голосовые ассистенты, автоматизация кол-центров, умные устройства. Здесь пользователь ожидает ответа в реальном времени, поэтому ключевая метрика — latency (задержка). Наша задача — минимизировать время между окончанием фразы и выдачей результата.

Как мы выстраиваем пайплайн:
Как видим выше, VAD получает входящий аудиопоток и сегментирует его на чанки. Как только детектируется речь, первый чанк направляется в обработку.
Чанк проходит препроцессинг, после чего поступает в онлайн-модель (Emformer). Ее задача — выдать промежуточное скрытое представление с минимальной задержкой.
-
Выход Emformer разделяется:
- одна копия идет в онлайн-декодер, который декодирует промежуточную гипотезу и возвращает ее клиенту (для обратной связи в реальном времени);
- вторая копия (вектора состояний) сохраняется в Storage — специализированном кеше, который хранит контекст на протяжении всей сессии.
Когда VAD детектирует конец речевого фрагмента, формируется финальный чанк. Система извлекает накопленные состояния из Storage и передает их вместе с финальным чанком в финализирующую модель (Conformer).
Conformer передает результат в финальный декодер, который выдает итоговую расшифровку пользователю.
На рисунке 3 представлена схема, где видно, что на протяжении активной речи онлайн-декодер выдает промежуточные гипотезы, — это критически важно для диалоговых сценариев.

Визуализация процесса выдачи финального распознавания представлена на рисунке 4. Когда VAD фиксирует конец фразы, формируется финальный чанк, который вместе с накопленными в Storage состояниями передается в связку «Conformer + финальный декодер». Эта связка выполняет итоговое распознавание с более высоким качеством, но ценой бóльших вычислительных затрат.

Режим офлайн работает на больших аудиосегментах (до 60 секунд) и применяется в сценариях, где скорость ответа уступает в приоритете объему обрабатываемых данных — например, при постобработке звонков или суммаризации записей. Здесь аудиопоток подается сегментами увеличенной длины, а результат возвращается клиенту по завершении обработки каждого сегмента.
Пайплайн состоит из того же набора моделей, но не в полном составе (см. рис. 5). Онлайн-декодер исключается из пайплайна: накопленные в Storage состояния после обработки Emformer сразу передаются в связку «Conformer + финальный декодер», минуя промежуточные выдачи.

Если резюмировать: ключевое различие между режимами сводится к компромиссу между задержкой и пропускной способностью. В онлайн-режиме приоритет отдается минимальной задержке, в офлайн-режиме — максимальному значению RTFx.
Продуктивизация
Ну и коротко о продуктивизации. В качестве основного фреймворка мы выбрали Triton Inference Server от NVIDIA. Это индустриальный стандарт, который закрывает ключевые требования к production-среде: безопасность и стабильность API, динамический батчинг запросов, поддержка CPU и GPU, возможность развертывания от edge-устройств до облачных кластеров. Если проводить аналогию, Triton выполняет роль диспетчерской: принимает запросы от клиентов и распределяет их между моделями внутри сервера. Модели (чекпоинты) подключаются к Triton через бэкенды. В нашей практике используются три основных бэкенда:
Бэкенд |
Назначение |
ONNX Runtime |
Универсальный, работает на CPU и GPU |
TensorRT |
Оптимизирован для GPU NVIDIA |
OpenVINO |
Решение от Intel, ориентировано на их железо |
Переход к продуктивизации невозможен без системы профилирования. Мы используем комбинацию встроенных средств Triton и собственных разработок. В частности, Triton предоставляет готовые решения для нагрузочного тестирования:
Perf Analyzer — замер latency и throughput для отдельных моделей.
Model Analyzer (до недавнего времени) — визуализация метрик и подбор оптимальных конфигураций.
Эти инструменты удобны для изолированных тестов, но не позволяют воспроизвести реальный сценарий работы ASR, где есть последовательный вызов нескольких моделей, промежуточные чанки и финализация. Для эмуляции реального пайплайна мы разрабатываем собственные бенчмарк-инструменты, которые отправляют запросы в Triton в точной последовательности, соответствующей бизнес-логике.
Для сквозного анализа задержек мы внедрили Jaeger. Он отслеживает время выполнения запроса от момента прихода на API до самой последней модели в пайплайне. Пример отчета Jaeger — на рисунке 6.

На скриншоте видна структура запроса:
Промежуточный чанк: ансамбль выполняется за 9 мс, с детализацией по каждой модели внутри. Клиент уже получает промежуточное распознавание.
Чанк финализации: здесь вызывается модель Conformer — она значительно тяжелее модели Emformer. Время выполнения составляет порядка 350 мс, что наглядно демонстрирует разрыв в вычислительной сложности между промежуточными и финальными этапами распознавания.
Далее, прежде чем рассказать о том, как мы оптимизировали модели под CPU, придется выдать немного теории — по метрикам оценки ASR и используемым инструментам для продуктивизации моделей. Если с метриками качества и производительности моделей распознавания вы уже знакомы, то скипайте эту часть.
Метрики оценки ASR
Метрики качества
Глобальная метрика, позволяющая сравнивать наше решение с другими вендорами и open-source-моделями, — Word Error Rate (WER). WER измеряет расхождение между референсной (эталонной) расшифровкой и гипотезой, выданной ASR. Чем меньше значение WER, тем ближе результат распознавания к идеальному. WER рассчитывается по формуле:
WER = (S + D + I) / N × 100%,
где S (substitutions) — количество замен, D (deletions) — количество удалений, I (insertions) — количество вставок, N — общее количество слов в эталонной расшифровке.
На первый взгляд WER кажется простой метрикой, но есть нюанс: ее можно считать двумя способами — с локальным усреднением (по предложениям) и с глобальным (по корпусу). Проблема локального усреднения: короткие предложения могут искажать общую картину.
Пример:
Предложение 1: 1 слово, 1 ошибка → WER = 100%
Предложение 2: 100 слов, 1 ошибка → WER = 1%
Локальное усреднение по предложениям: (100% + 1%) / 2 = 50,5% — это не отражает реальную точность системы.
Глобальный расчет по корпусу: сумма всех ошибок (2) / сумма всех слов (101) × 100% ≈ 1,98%. Такой подход минимизирует влияние выбросов и дает объективную оценку.
На рисунке 7 представлена формула для расчета WER, а также характерные значения WER для различных условий записи.

Дополнительный аспект: требования к детализации
У разных заказчиков разное представление о «качественном распознавании». Одним важно получать детальную расшифровку всего аудиоканала: фоновую речь, хезитации, лишние звуки. Другим — только целевую речь клиента. Например, голосовая колонка должна реагировать только на обращенный к ней запрос, игнорируя разговоры окружающих. Поэтому настройка ASR всегда требует учета бизнес-контекста.
Метрики производительности
Мы используем две основные метрики производительности в зависимости от режима работы.
Для онлайн-распознавания — Last Chunk Latency (LCL), задержка последнего чанка. LCL измеряет интервал от момента отправки последнего чанка аудио до момента получения финальной гипотезы.
Для офлайн-режима — Real-time factor (RTFx), пропускная способность. RTFx рассчитывается как продолжительность распознанного аудио, деленная на затраченное время вычислений.
Таким образом, в онлайн-режиме мы стремимся минимизировать LCL, в офлайн-режиме — максимизировать RTFx. Эти метрики находятся в компромиссе: оптимизация под одну из них часто ухудшает другую.
Перед переходом к бенчмаркам зафиксируем конфигурации тестовых стендов (рис. 8). Наша цель — оценить возможность переноса инференса моделей с GPU на CPU, поэтому сравнение проводилось между двумя вариантами стендов.

Прокси-метрики
WER — это прямая метрика качества ASR, но на нее влияют компоненты, которые не входят в ее формулу. Ошибки этих компонентов могут как ухудшить, так и улучшить итоговый WER, поэтому мы ввели собственные прокси-метрики для детального профилирования.
Для оценки работы VAD мы используем две метрики:
False Positive Rate (FPR) — доля тишины, ошибочно переданной в ASR как речь.
False Negative Rate (FNR) — доля речи, ошибочно отсеченной VAD.
На рисунке 9 приведен пример: реальный речевой сегмент выделен зеленым, а сегмент, переданный VAD в ASR, отмечен штриховкой. В данном случае 20% речи утеряно (FNR) и 10% лишней тишины передано (FPR). Это увеличивает разрыв между фактической и распознанной речью и снижает качество.

Пример реального кейса
Представьте звонок в кол-центр. Клиент произносит: «Оператор, соедините меня». Если VAD обрежет начало фразы, до ASR дойдет только «...ератор, соедините меня» или даже «...пират...». В результате система может не распознать ключевое слово «оператор» и дальнейший пайплайн (например, маршрутизация через LLM) отработает некорректно.
Для анализа задержек мы также ввели набор прокси-метрик. Основной показатель, который видит пользователь, — Total Latency: задержка от момента, когда он закончил говорить, до момента получения ответа от платформы.
Однако для диагностики этого недостаточно. Если смотреть на работу VAD, то препроцессинг, время инференса модели и постпроцессинг являются константными (они зашиты в модель). Переменная составляющая — Finalization Gap: период от конца речи до момента, когда VAD предсказал метку конца фразы. Эту метрику важно минимизировать. Поэтому мы ввели комплексную систему из 10 метрик для оценки качества работы VAD:
Метрика |
Описание |
Cutoff Begin / End |
Предсказание модели пересекается с сегментом речи, но метка начала/конца фразы находится позже или раньше реальной |
False Negative Begin / End |
Модель не обнаружила речь там, где она есть (начало или конец фразы) |
False Positive Begin / End |
Модель обнаружила речь там, где ее нет (начало или конец фразы) |
Early Activation (сек.) |
Метка начала фразы находится раньше реального начала |
Finalization Gap (сек.) |
Метка конца фразы находится позже реального конца |
True Positive Begin / End |
Метка начала/конца фразы находится в допустимом диапазоне |
Резюмируя раздел про метрики, рекомендую:
Оценку качества WER проводить глобально (по корпусу), а не усреднением по предложениям.
Обращать внимание на созависимые компоненты, которые могут повлиять на целевые метрики сервиса.
Вводить собственные прокси-метрики для детального профилирования даже на этапе продуктивизации, когда, казалось бы, все уже работает в проде.
Оптимизация. Инференс на CPU
С введением закончили — теперь к главной теме.
Процесс оптимизации мы выстроили по классическому циклу: замеряем, находим узкое место, применяем изменение, повторяем замеры. Инструментарий — собственные бенчмарк-утилиты, Perf Analyzer, nsys, htop.
Типовая модель в Triton состоит из четырех этапов:
Очередь к модели.
Обработка входов.
Вычисления (инференс).
Обработка выходов.
Если основное время тратится на вычисления, оптимизация направлена либо на модель (квантизация, смена бэкенда), либо на код (если внутри модели есть кастомные операции).
Рассмотрим два конкретных примера из нашей практики.
Пример 1. Оптимизация TDT-декодера
В пайплайне ASR финальным звеном является декодер. Его задача — преобразовать векторы, полученные от Conformer, в текст. В нашей реализации это код C++ с stateful-состоянием (переиспользование памяти).

В декодере работают:
Predictor — предсказывает скрытое состояние на основе токена.
Joint — объединяет текстовые и аудиовыходы для итогового распознавания.
WFST-граф — для лексической обработки.
Методология сравнения
Чтобы оценить влияние декодера на производительность, мы ввели идеальный ориентир — пайплайн без декодера. Клиенту такой вариант не нужен (он получает не текст, а векторы), но он показывает теоретический предел производительности.
Исходные значения (без декодера):
Онлайн: 175 RTFx.
Офлайн: 430 RTFx.
Все дальнейшие изменения мы оцениваем через дельту относительно этого идеала. Результаты экспериментов сведены в таблицу на рисунке 11.

Шаг 1. Декодер на GPU (FP32)
Возвращаем декодер в пайплайн. Исходная конфигурация — GPU, полная точность FP32. Получили дельту: −15,5% — онлайн; −30% — офлайн.
Шаг 2. Декодер на GPU. Оптимизация внутри декодера
Пробуем перенести расчет argmax внутрь joint — результата не дало (время копирования незначительно, основное влияние оказывает расчет).
Переводим операции перемножения векторов на FP16 — получаем улучшение производительности. Переводим сами модели (predictor и joint) на FP16 — производительность усиливается. Дельта сократилась до −7,4% (онлайн) и −20% (офлайн).
Шаг 3. Декодер на GPU. Увеличение числа CPU
Замеры проводились на конфигурации с одним ядром CPU и MIG-профилем. Пробуем увеличить число CPU-ядер до восьми (как в продовой среде). Результат: дельта практически не меняется. Увеличение CPU-ресурсов не помогает — узкое место остается в GPU.
Шаг 4. Декодер на CPU
Переносим декодер на CPU. Сравниваем два образа сервера: без декодера (идеал) и с декодером на CPU. Получили дельту: онлайн — −6,6%, офлайн — −4%.
Вывод
Сравнение четырех конфигураций (GPU онлайн, CPU онлайн, GPU офлайн, CPU офлайн) показывает, что инференс на GPU не всегда оправдан. Для некоторых компонентов (например, декодера) CPU может давать лучшую производительность за счет отсутствия накладных расходов на передачу данных между GPU и CPU (PCIe overhead) и дорогостоящих операций синхронизации (host-to-device / device-to-host transfers). Смотрите сравнение на рисунке 12.

Пример 2. Оптимизация Storage
Storage — это специализированный кеш, расположенный между Emformer и Conformer. Его задача — накапливать выходы Emformer и передавать их в Conformer в момент финализации. Архитектурное расположение Storage в пайплайне показано на рисунке 13.

Исходная конфигурация (CPU)
Изначально Storage был реализован на CPU по двум причинам:
Мы стремились снизить потребление ресурсов GPU.
Выходы Emformer в любом случае копируются на CPU для возврата клиенту, поэтому казалось логичным разместить Storage там же.
Однако профилирование показало, что Storage — узкое место. Решили его оптимизировать.
Шаг 1. Перенос на GPU
Мы перенесли Storage на GPU, используя простой перенос тензоров через .to(device). Результат — прирост производительности почти 50%: RTFx вырос с 350 до 590–620 в зависимости от железа. Результаты и схема переноса представлены на рисунке 14.

Казалось бы, успех, — можно катить в прод.
Шаг 2. Откат после обновления Triton
При обновлении Triton с версии v24.05 на v24.09 мы обнаружили резкую потерю достигнутого ускорения. Детальное профилирование через nsys показало, что код, дававший буст на старой версии, на новой вызывает огромные оверхеды синхронизации. Сравнение профилей представлено на рисунке 15.

Это наглядный пример того, насколько хрупкой может быть низкоуровневая оптимизация, зависящая от внутренней реализации фреймворка.
Шаг 3. Переход на stateful-состояние
Мы отказались от идеи выделенной модели Storage и перевели его в stateful-состояние — блок памяти, встроенный непосредственно в Emformer. Storage перестал быть отдельной моделью в Triton, что исключило накладные расходы на межмодельные вызовы.
Реализация stateful-подхода дала дополнительный прирост производительности 10–20%. Сравнение baseline и stateful-версии приведено на рисунке 16.

Цена изменений
Переход на stateful потребовал корректировки ресурсов:
Увеличение CUDA pool до 2.5 GB.
Увеличение pinned memory до 1.5 GB.
Также уменьшился максимальный буфер обработки длины аудио: с >60 секунд до 52 секунд. Это не стало ограничением для production, поскольку VAD в любом случае режет сегменты по 30 секунд.
Пример 3. Перевод нейросетевых моделей на CPU
Для инференса моделей на CPU мы рассматривали два бэкенда: ONNX Runtime (универсальный) и OpenVINO (решение от Intel). Эксперименты проводились по следующему сценарию:
Этап |
Действие |
Результат |
1 |
ONNX Runtime (FP32) |
Качество сохранено, производительность низкая |
2 |
ONNX Runtime + динамическая квантизация INT8 |
Хорошее ускорение, качество сохранено |
3 |
ONNX Runtime + статическая квантизация |
Не пробовали — требует калибровочного датасета, который не всегда репрезентативен для разных клиентов |
4 |
OpenVINO (FP32) |
Скорость сопоставима с ONNX Runtime FP32 |
5 |
OpenVINO + INT8 |
Качество сохранено, производительность приемлемая |
6 |
OpenVINO на другом железе |
Обнаружена зависимость от наличия инструкций AVX-512 — без них производительность падает |
Неожиданный результат
Мы ожидали, что OpenVINO (вендорское решение от Intel) будет работать быстрее универсального ONNX Runtime, но на тестовом железе скорости оказались сопоставимы. Однако при попытке развертывания на другом оборудовании выяснилось, что производительность OpenVINO критически зависит от наличия AVX-512 — набора инструкций для распараллеливания вычислений на ядре. Без него OpenVINO работал медленно, в то время как ONNX Runtime демонстрировал стабильную производительность независимо от наличия этих инструкций.
Выводы из вышеперечисленных примеров:
GPU не всегда лучше CPU. Для некоторых компонентов (например, декодера) перенос на CPU дает производительность, близкую к идеалу, за счет отсутствия PCIe overhead и накладных расходов на синхронизацию.
Оптимизация зависит от стека. Эффективность изменений критически зависит от версий Triton, драйверов CUDA, типа GPU (A100 vs A40 vs V100) и CPU (наличие AVX-512). Всегда важно тестировать на целевых конфигурациях серверов.
Низкоуровневое профилирование — обязательно. Без nsys и GPU-профилирования сложно выявить реальные причины изменений производительности, особенно в случаях, когда обновление фреймворка (Triton v24.05 → v24.09) ломает ранее работавшие оптимизации.
Стабильность важнее хаков. Оптимизации, основанные на недокументированном поведении, ненадежны. При переходе на stateful-хранение мы получили устойчивый прирост 10–20%, не привязанный к конкретной версии фреймворка.
Бенчмарк моделей
До этого мы говорили об ASR «в вакууме». В реальности мы поддерживаем две модели со схожей архитектурой, но разного размера.
Характеристика |
Модель A |
Модель B |
Декодер |
ALSD |
TDT |
Параметры |
125M |
340M |
WER (телефонный канал) |
~11.6% |
< 10% |
Примечание |
Базовый вариант |
Топ для телефонного канала |
Модель B использует более эффективный декодер TDT, но Conformer в ней увеличен почти в два раза, что дает улучшение качества (WER < 10%) ценой роста вычислительной сложности.
Тестирование на CPU
Запускаем обе модели на CPU (8 ядер, без GPU) и варьируем распределение инстансов между моделями Emformer и Conformer. Результаты производительности, потребления ресурсов и WER для различных наборов инстансов представлены на рисунке 17.

Итоговые показатели для production-сценариев
Модель |
Количество клиентов |
LCL |
RTFx |
125M |
130 |
266 мс |
180 |
340M |
~100 |
304 мс |
140 |
Сравнение GPU vs CPU
На рисунке 18 приведено сравнение базовой GPU-конфигурации и CPU-конфигурации. CPU стабильно обрабатывает 130 клиентов. GPU в базовой конфигурации показывает 200 клиентов, а после последних оптимизаций — уже 400 клиентов. Эти цифры можно напрямую переводить в экономику: оценивать утилизацию ресурсов и стоимость владения.

Сравнение с Open-source
Мы обнаружили репозиторий onnx-asr, где собраны почти все русскоязычные open-source ASR-модели с бенчмарками на студийном датасете LibriSpeech. Выражаем благодарность автору, Илье Ступакову, за проделанную работу. Сравнение представлено на рисунке 19.

Ключевые наблюдения:
Модель от GigaAM показывает WER 5,2% (наша модель на этом датасете работает хуже: мы не обучались на студийных данных, наша специализация — телефонный канал).
Whisper выдает всего 0,4 RTFx — катастрофически мало, нужна серьезная оптимизация, чтобы модель была применима в проде.
Интересно соотношение числа параметров и RTFx: наша модель 340M (оптимизированная) показывает RTFx 47, в то время как некоторая open-source модель на 120M параметров выдает лишь 41 RTFx. Это наглядно демонстрирует важность продуктивизации и оптимизации.
Сравнение на доменном канале
На рисунке 20 показано сравнение WER на нашем целевом телефонном канале. Естественно, мы работаем лучше опенсорса, потому что обучались на этих данных. Однако некоторые open-source-решения также показывают достойные результаты. Важно помнить: не все из них поддерживают онлайн-режим.

Резюме
Для тех, кто дочитал до конца, но пост настолько длинный, что забыл, о чем читал, вот резюме:
Считайте WER глобально, а не усреднением по предложениям — иначе короткие предложения искажают картину.
Вводите прокси-метрики (FPR/FNR для VAD) — финальный WER не показывает проблемы компонентов.
GPU не всегда лучше CPU: декодер на CPU дал дельту 4–6% от идеала, обогнав GPU-версию за счет отсутствия PCIe overhead.
Оптимизации хрупки: обновление Triton сломало перенос Storage на GPU. Решение — stateful-состояние (устойчивый прирост 10–20%).
Выбирайте бэкенд под железо: OpenVINO требует AVX-512, ONNX Runtime стабильнее на разных CPU.
Продуктивизация окупается: наша модель 340M (RTFx 47) обошла open-source модель 120M (RTFx 41) за счет оптимизации.
Итог: перевод ASR на CPU без потери качества возможен, но требует системного подхода к метрикам, профилированию и тестированию на целевых конфигурациях.
Благодарность
Хочу поблагодарить всю команду Audiogram MWS AI, без которой этой статьи и доклада не получилось бы. Отдельное спасибо за вклад в исследование, разработку и отличную реализацию!
Список литературы