- Зачем: - нужно выбрать реалистичный способ ускорения эмбеддингов на разных x86 CPU. - Что: - зафиксированы ограничения batch, stream/session и static INT8 для трех моделей. - рекомендованы два прототипа и минимальная матрица CPU/ISA. - Проверка: - git diff --cached --check.
21 KiB
Ускорение извлечения голосовых эмбеддингов на x86 CPU
Дата: 2026-08-30
Статус: исследовательская записка. Production-код и конфигурация не менялись.
Метод: проверены sherpa-onnx==1.13.5, onnxruntime==1.28.0, официальные
скрипты экспорта и исходники владельцев моделей. ONNX-модели не скачивались.
Формы ниже подтверждены экспортными контрактами, проверками метаданных и кодом
загрузчика. Файлы из выпуска повторно не разбирались.
Вопрос и краткий ответ
Текущий путь не использует 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 | Один float32-вход [B, T, 80], один выход [B, D]. Скрипт, которым sherpa-onnx добавляет метаданные, проверяет символические B, T, размерность 80 и единственный выход. Метаданные включают framework=wespeaker, sample_rate, output_dim, normalize_samples=0 (проверка sherpa-onnx, экспорт WeSpeaker). |
Равные T можно сложить по B. Длины или маску граф не принимает; дополнение участвует в pooling и меняет эмбеддинг. |
| CAMPPlus zh/en | Экспорт создает float32-вход [N, T, 80], метаданные framework=3d-speaker, output_dim, normalize_samples=1, feature_normalize_type=global-mean (скрипт sherpa-onnx). |
В скрипте есть ошибка: выход называется embedding, а dynamic axis задан для embeddings. Поэтому динамичность оси выхода файла из выпуска по исходнику не доказана. Upstream подтверждает лишь, что batch равной длины должен работать; для разных длин нужна маска (ответ разработчика). |
| NeMo TitaNet small | Два входа: float32 [N, C, T] и int64 [N] с длинами; embedding имеет форму [N, D] (контракт C++-обертки). Метаданные содержат framework=nemo, output_dim, feat_dim, sample rate, параметры окна и нормализации (экспорт). |
Граф имеет нужный контракт для batch переменной длины с дополнением. Но sherpa-onnx создает обе входные формы с N=1 и возвращает только первую строку (реализация). |
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 | Граф WeSpeaker поддерживает равные длины; CAMPPlus требует прямой проверки файла из выпуска; TitaNet поддерживает разные длины через второй вход. 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 получает собственные пулы потоков и арены памяти. ORT прямо отмечает эти накладные расходы для двух session (ответ сопровождающего). | Не брать как самостоятельный кандидат. Допустима одна контрольная ячейка против 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 предупреждает:
- на AVX2 и AVX-512 без VNNI путь U8S8 использует
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.
- Фактическая batch-форма выхода CAMPPlus release asset из-за ошибки в экспортном скрипте.
- Распределение
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 и слов без говорящего.
U8U8 или reduce_range добавлять лишь при подтвержденной потере точности на
AVX2. В первую матрицу эти варианты не входят.
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-матрица для обоих непосредственных кандидатов:
| Класс | Конкретный доступный ориентир | Зачем |
|---|---|---|
| AVX2, без VNNI/AVX-512 | Intel Core i7-6820HQ, 4C/8T | Проверить нижнюю границу INT8 и конкуренцию на старом CPU. |
| VNNI + AVX-512 | AMD Ryzen 7 8845H из предыдущих замеров, с обязательной фиксацией lscpu/CPUID |
Проверить специализированный INT8 kernel и масштабирование на 8C/16T. |
Фактический целевой Core i5 11-го поколения добавить как приемочную машину, когда он появится; перед запуском записать точную модель и CPUID. Двух машин достаточно, чтобы решить судьбу кандидата на двух ISA-классах. Переносить полученный коэффициент ускорения на весь x86 нельзя.