Files
local-transcriber/docs/research/2026-08-30-diarization-embedding-inference.md
T
Dmitry Dementiev 2fc777fdee docs(cpu): уточнена аппаратная матрица
- Зачем:
  - Конкретный Core i5 ошибочно стал обязательной приемочной машиной при разнородном парке пользователей.
- Что:
  - CPU-дефолт отделен от воспроизводимых Intel и AMD baseline.
  - README, backlog и исследование приведены к единой терминологии.
- Проверка:
  - `git diff --check`.
2026-08-30 20:58:18 +03:00

23 KiB
Raw Blame History

Ускорение извлечения голосовых эмбеддингов на 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 за вызов.

Из трех направлений достойны прототипа два:

  1. статическая INT8-квантизация действующего WeSpeaker в формате S8S8 QDQ;
  2. несколько независимых 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 нельзя.