- Зачем: - Конкретный Core i5 ошибочно стал обязательной приемочной машиной при разнородном парке пользователей. - Что: - CPU-дефолт отделен от воспроизводимых Intel и AMD baseline. - README, backlog и исследование приведены к единой терминологии. - Проверка: - `git diff --check`.
23 KiB
Ускорение извлечения голосовых эмбеддингов на x86 CPU
Дата: 2026-08-30
Статус: исследовательская записка. Production-код и конфигурация не менялись.
Метод: проверены sherpa-onnx==1.13.5, onnxruntime==1.28.0, официальные
скрипты экспорта и исходники владельцев моделей. Три официальных release asset
прочитаны потоком без сохранения на диск: WeSpeaker ResNet34 LM, CAMPPlus en и
TitaNet small. Их входы, выходы и метаданные разобраны через ORT 1.28.0; для
каждого выполнен CPU smoke-test с batch size 2.
Вопрос и краткий ответ
Текущий путь не использует batch: sherpa-onnx последовательно создает stream и
вызывает один Session::Run для каждой пары "окно, локальный говорящий".
Публичный SpeakerEmbeddingExtractor также принимает один stream за вызов.
Из трех направлений достойны прототипа два:
- статическая INT8-квантизация действующего WeSpeaker в формате S8S8 QDQ;
- несколько независимых stream поверх одной ORT session с ограниченным общим числом потоков.
Настоящий batch технически лучше всего подготовлен у TitaNet: граф принимает вектор длин. Но TitaNet пока допустим только при явно известном числе говорящих, а sherpa-onnx все равно фиксирует batch size 1. Поэтому batch TitaNet - условный следующий прототип; действующую конфигурацию он не ускоряет.
Несколько отдельных session не стоит прототипировать первым: это не уменьшает работу, создает отдельные пулы потоков и арены памяти, повышает риск oversubscription и роста RSS. Для WeSpeaker и CAMPPlus batch с простым дополнением также не готов: их графы не принимают длины или маску.
Что именно выполняется последовательно
Приложение создает один OfflineSpeakerDiarization и задает одно и то же
num_threads отдельно для segmentation и embedding
(адаптер проекта). Внутри
sherpa-onnx стадии идут строго segmentation → embeddings → clustering, а
ComputeEmbeddings проходит список пар обычным циклом: создает stream,
добавляет все неперекрывающиеся фрагменты одного локального говорящего,
завершает input и синхронно вызывает Compute
(реализация Pyannote в sherpa-onnx 1.13.5).
Публичный C++ и Python API экспортирует create_stream, is_ready и
compute(stream). Метода compute_batch(streams) нет
(C++ API,
Python binding).
Python binding освобождает GIL на compute, поэтому внешний параллельный вызов
в принципе возможен; готовый диаризатор промежуточные stream наружу не отдает.
Для embedding создается одна ORT session. Sherpa-onnx передает заданное число и
в intra_op_num_threads, и в inter_op_num_threads
(настройка session).
ORT по умолчанию исполняет граф последовательно; inter_op начинает
распараллеливать узлы только при ORT_PARALLEL, тогда как intra_op
распараллеливает оператор
(официальная документация ORT).
Значит, в текущем CPU-пути основная ручка - intra_op.
Контракты трех графов
| Модель | Вход и выход официального экспорта | Следствие для batch |
|---|---|---|
| WeSpeaker ResNet34 LM | Release asset имеет один float32-вход [B, T, 80] и выход [B, 256] (asset). Скрипт, которым sherpa-onnx добавляет метаданные, проверяет символические B, T, размерность 80 и единственный выход. Метаданные включают framework=wespeaker, sample_rate, output_dim, normalize_samples=0 (проверка sherpa-onnx, экспорт WeSpeaker). |
Равные T можно сложить по B. Длины или маску граф не принимает; дополнение участвует в pooling и меняет эмбеддинг. |
| CAMPPlus en | Release asset имеет float32-вход [N, T, 80] и выход с динамической первой осью [*, 512] (asset). Метаданные: framework=3d-speaker, output_dim=512, normalize_samples=1, feature_normalize_type=global-mean (экспорт sherpa-onnx). |
Несмотря на опечатку экспортного скрипта (embedding/embeddings), release asset принимает batch равной длины и возвращает все строки. Для разных длин граф по-прежнему не принимает маску или длины. |
| NeMo TitaNet small | Release asset имеет входы float32 [N, 80, T] и int64 [N]; выходы — logits [N, 16681] и embedding [N, 192] (asset, контракт C++-обертки). Метаданные содержат framework=nemo, output_dim, feat_dim, sample rate, параметры окна и нормализации (экспорт). |
Граф принимает batch и вектор длин. Но sherpa-onnx создает обе входные формы с N=1 и возвращает только первую строку (реализация). |
Smoke-test с синтетическими float32-признаками и одним intra-op thread дал
для WeSpeaker (2, 256), для CAMPPlus (2, 512) и для TitaNet (2, 192).
У WeSpeaker и CAMPPlus результат batch size 2 совпал с конкатенацией двух
последовательных Run (max_abs_diff=0). У TitaNet элемент длиной 100 кадров
также совпал, но элемент длиной 80, дополненный до 100 кадров, отличался от
отдельного непаддированного вызова (max_abs_diff=0,0269). Это не тест качества
на голосе, а подтверждение, что допустимый порог для variable-length batch нужно
определить в прототипе, а не считать эквивалентность гарантированной контрактом.
WeSpeaker ResNet34 состоит из Conv2d-блоков и statistics pooling (исходник WeSpeaker); CAMPPlus сочетает Conv2d, TDNN/Conv1d и statistics pooling (исходник 3D-Speaker); TitaNet использует depth-wise separable Conv1d, squeeze-and-excitation и attention statistics pooling (документация NeMo). Этого недостаточно, чтобы утверждать: каждый оператор будет квантизован и ускорен.
В официальном выпуске speaker-recognition models есть только FP32-файлы WeSpeaker, CAMPPlus и TitaNet; готовых INT8 embedding assets нет. Статический INT8 здесь означает собственный производный артефакт с отдельной проверкой лицензии, метаданных и качества.
Оценка способов ускорения
| Способ | Техническая доступность | Вердикт |
|---|---|---|
| Настоящий batch | Release-графы WeSpeaker и CAMPPlus поддерживают batch равной длины; TitaNet принимает batch и второй вход с длинами. Sherpa-onnx для всех трех жестко формирует N=1 и не имеет batch API. |
Прототипировать только TitaNet и только условно для режима с известным числом говорящих. Для WeSpeaker/CAMPPlus без маски возможны лишь группы с одинаковым T; ожидаемая наполняемость неизвестна. |
| Несколько stream, одна session | Stream независимы, Compute использует локальные тензоры, а ORT разрешает нескольким потокам одновременно вызывать Run одной session (архитектура ORT, контракт 1.28.0). Текущий цикл sherpa-onnx этого не делает. |
Достойно узкого прототипа на WeSpeaker. У одной session общий intra-op pool; нужно сравнить внутренний и внешний параллелизм. Полный pool для каждого worker не нужен. |
| Несколько session | Можно создать несколько extractor, но по умолчанию каждая session получает собственный intra-op pool и CPU arena. ORT отдельно предлагает общий pool против конкуренции session pools и общий allocator против роста памяти (thread management, C API guide). | Не брать как самостоятельный кандидат. Допустима одна контрольная ячейка против shared-session, чтобы подтвердить отказ по реальному времени и RSS. |
| Статический INT8 | ORT предоставляет quantize_static, использует calibration data и рекомендует static quantization для CNN. Для CPU первым выбором служит S8S8 QDQ; квантизация может ухудшить точность и даже скорость (официальное руководство). |
Главный кандидат для действующего WeSpeaker. CAMPPlus и TitaNet квантизовать только после продуктового решения использовать соответствующую модель. |
Batch сокращает число Session::Run и может лучше загрузить SIMD, но число
эмбеддингов и основная модельная арифметика сохраняются; padding может даже
добавить работу. Несколько stream или session лишь перекрывают независимые
вызовы. Только INT8 меняет арифметику и объем движения весов. Увеличение шага
окна или отказ от части эмбеддингов действительно сокращают работу. Их нужно
рассматривать в отдельном исследовании шага окна.
ISA, oversubscription и память
AVX2, VNNI и AVX-512 служат уровнями runtime dispatch одного формата модели. MLAS в ORT 1.28.0 проверяет CPUID во время запуска и выбирает отдельные AVX2, AVX-VNNI, AVX-512 и AVX512-VNNI kernels (runtime dispatch). Один QDQ-граф поэтому нужно измерять на разных CPU. Отдельная сборка под каждый из них не нужна.
Официальное руководство ORT предупреждает:
- S8S8 QDQ остается первым выбором CPU. Если он дает значимую потерю точности, ORT советует проверить U8U8;
- отдельный путь U8S8 на AVX2 и AVX-512 без VNNI использует
VPMADDUBSWи может дать saturation; если до него дойдет матрица, этот риск снижаютreduce_rangeили переход на U8U8; - на x86 с VNNI этой проблемы нет, а INT8 обычно выигрывает больше;
- на старом CPU без подходящих инструкций quantize/dequantize overhead способен сделать модель медленнее (разделы Data type selection и FAQ).
Доступный Core i7-6820HQ официально заявляет только SSE4.1/4.2 и AVX2
(Intel ARK).
Современный класс должен выбираться по фактическим CPUID-флагам. Надписи
"11-е поколение" недостаточно: минимум нужен avx512_vnni или avx_vnni. Для доступного
Zen 4 AMD документирует AVX-512 и AVX512_VNNI как путь ускоренного integer GEMM
(AMD AOCL).
В параллельном прототипе нужно измерять RSS. Уже последовательный тест показал,
что TitaNet требует примерно на 147–149 МиБ больше RSS, чем WeSpeaker, а FP32
WeSpeaker на старом Intel использовал в среднем 6,84 логического ядра из восьми
(локальный benchmark).
Конкурентные Run одной session делят общий intra-op pool, но все равно
конкурируют за его очереди и добавляют вызывающие потоки. Несколько session уже
создают отдельные пулы и дополнительно размножают арены и подготовленные веса.
Точный рост не следует считать линейным - его нужно измерить.
Что остается неизвестным
- Реальный процент Conv/MatMul, который ORT сможет перевести в INT8, и выигрыш каждого графа на двух ISA-классах.
- Сохранятся ли обязательные пользовательские метаданные после квантизации; без
framework,output_dimи параметров обработки признаков sherpa-onnx модель не загрузит. - Точность статического INT8 после калибровки и необходимость исключить отдельные узлы через quantization debugging.
- Распределение
Tу пар "окно, локальный говорящий". Без него нельзя оценить полезность exact-length buckets для WeSpeaker/CAMPPlus. - Выигрыш shared-session concurrency после ограничения общего числа потоков, его пиковый RSS и стабильность повторных конкурентных вызовов TitaNet.
Рекомендация и минимальная граница прототипа
1. INT8 WeSpeaker - первый кандидат
На отдельном стенде, не меняя загрузчик проекта:
- выполнить
quant_pre_process, затемquantize_staticв S8S8 QDQ; - проверить opset, вход/выход, все пользовательские метаданные и загрузку через
SpeakerEmbeddingExtractor; - сравнить FP32 и INT8 по реальному времени embedding-стадии, процессорному времени, пиковому RSS и расстоянию между эмбеддингами;
- только после технического выигрыша повторить действующие проверки числа голосовых кластеров, purity и слов без говорящего.
При значительной потере точности S8S8 проверить U8U8. Отдельный U8S8 с
reduce_range в первую матрицу не входит.
2. Shared-session streams - второй кандидат
Изолированный стенд или минимальная upstream-поправка должен сохранить одну модель и одну ORT session, подготовить независимые stream и сравнить:
outer workers / shared intra-op pool: 1 / P, 2 / P, P / 1
inter-op threads: 1
Здесь P - число физических ядер. Первая ячейка показывает текущий внутренний
параллелизм, вторая проверяет конкурентные Run на общем pool, третья переносит
параллелизм наружу и не создает intra-op pool. Измерять нужно полный набор
независимых embedding-вызовов. Latency одного удачного stream для решения
недостаточно. Отдельные session оставить одной отрицательной контрольной ячейкой
P workers / 1 thread.
3. Batch TitaNet - условный кандидат
Если отдельное решение разрешит TitaNet для режима с известным числом
говорящих, прямой ORT-стенд должен сначала доказать на размерах batch 1 / 4,
что batch переменной длины с дополнением и x_lens дает те же эмбеддинги в
заданном допуске, что и последовательные вызовы. Затем измерить пропускную
способность и RSS. До этого менять
sherpa-onnx API ради TitaNet преждевременно. CAMPPlus и batch без mask в эту
границу не входят.
Минимальная доступная CPU-матрица для обоих непосредственных кандидатов:
| Класс | Конкретный доступный ориентир | Зачем |
|---|---|---|
| Консервативный x86 baseline: AVX2, без VNNI/AVX-512 | Intel Core i7-6820HQ, 4C/8T | Проверить нижнюю границу INT8 и конкуренцию на старом CPU. |
| Современный x86 baseline: VNNI + AVX-512 | AMD Ryzen 7 8845H из предыдущих замеров, с обязательной фиксацией lscpu/CPUID |
Проверить специализированный INT8 kernel и масштабирование на 8C/16T. |
Эти две машины доступны для воспроизводимых прогонов; ни одна из них не является целевым SKU проекта. Ноутбук с Intel Core i5 11-го поколения - лишь пример распространенного пользовательского класса и не обязательная приемочная машина. Для первого отбора кандидата двух ориентиров достаточно; на любой дополнительной машине нужно записывать точную модель и CPUID. Переносить полученный коэффициент ускорения на весь x86 нельзя.