Files
local-transcriber/docs/research/2026-08-14-diarization-cpu-cost.md
T
Dmitriy Dementiev 98baf55572 docs(diarization): зафиксированы результаты приёмки и анализа
- Зачем:
  - решения по неопределённым словам и CPU-стоимости должны быть проверяемыми до публикации реализации.
- Что:
  - задокументировано отклонение post-hoc сглаживания Speaker ? по соседним кластерам.
  - добавлено исследование стоимости каскада и облегчённых embedding-моделей.
- Проверка:
  - `git diff --cached --check` — без ошибок.
2026-08-14 21:56:10 +03:00

200 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Стоимость CPU-диаризации и облегчённые альтернативы
**Дата:** 2026-08-14
**Статус:** исследовательская записка. Production-конфигурация не менялась.
## Краткий вывод
Измеренная стоимость ожидаема для выбранного каскада, но не является
неизбежным минимумом. `WeSpeaker ResNet34 LM` — не аномально большая модель:
ONNX-файл занимает 26,5 МБ, а сама архитектура находится в младшей части
семейства WeSpeaker ResNet. Основная цена возникает из способа применения
моделей: Pyannote проходит запись перекрывающимися 10-секундными окнами с шагом
1 секунда, после чего sherpa-onnx отдельно считает speaker embedding для каждого
локального говорящего каждого окна.
На минутном профиле текущей связки на этом ноутбуке эмбеддинги заняли 5,150 с
из 6,395 с, сегментация — 1,243 с, кластеризация — меньше измеримой миллисекунды.
То есть около 81% времени в этом прогоне пришлось на многократные вызовы
WeSpeaker. Ускорять только кластеризацию или менять её порог бессмысленно.
Более лёгкие пути есть. Самый безопасный для исследования — официальная INT8
версия той же Pyannote-сегментации. Самый большой подтверждённый upstream
потенциал даёт `nemo_en_titanet_small`, но её качество и порог кластеризации на
русских созвонах проекта ещё не проверялись. Локально проверенная CAMPPlus zh/en
быстрее WeSpeaker примерно на 30%, однако не прошла требование автоматического
определения числа говорящих с единым порогом.
## Откуда берётся стоимость
`sherpa-onnx` строит результат из трёх вычислительных стадий: speaker
segmentation, speaker embeddings и clustering. Это соответствует как
[официальному API](https://k2-fsa.github.io/sherpa/onnx/c-api/html/speaker_diarization.html),
так и [реализации конвейера
Pyannote](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h).
### 1. Перекрывающаяся сегментация
Pyannote segmentation 3.0 принимает 10 секунд mono 16 кГц и различает до трёх
локальных говорящих в окне, включая пары одновременно говорящих. Это явно
зафиксировано в [карточке исходной
модели](https://huggingface.co/pyannote/segmentation-3.0).
В официальном ONNX-файле `window_size=160000`, то есть те же 10 секунд при
16 кГц. В sherpa-onnx 1.13.5 `window_shift_ratio` по умолчанию равен 0,1, а
число окон вычисляется из размера окна и этого шага. Поэтому обычная длинная
запись проходит через segmentation-модель примерно десятикратно
перекрывающимися окнами: новое окно начинается каждую секунду. См.
[конфигурацию шага](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-segmentation-pyannote-model-config.h)
и [цикл обработки
окон](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h).
Это не признак «огромной» модели: FP32-файл сегментации весит лишь 5,7 МБ.
Стоимость создаёт прежде всего частота её запуска. Официальная поставка также
содержит `model.int8.onnx` размером 1,5 МБ
([документация sherpa-onnx](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/models.html)).
### 2. Embedding для каждого локального говорящего каждого окна
После сегментации sherpa-onnx исключает перекрывающиеся кадры из материала для
эмбеддинга, собирает пары `(окно, локальный говорящий)` и для каждой пары создаёт
отдельный stream и запускает embedding extractor. Это видно непосредственно в
[методах `GetChunkSpeakerSampleIndexes` и
`ComputeEmbeddings`](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h).
При одном активном голосе это уже примерно один embedding на секунду записи;
если в окне модель видит несколько локальных голосов, вызовов становится больше.
Текущий extractor — английский VoxCeleb `ResNet34_LM`; WeSpeaker поясняет, что
суффикс LM означает дополнительную large-margin донастройку, полезную на
фрагментах длиннее трёх секунд
([официальный список моделей](https://github.com/wenet-e2e/wespeaker/blob/master/docs/pretrained.md)).
### 3. Кластеризация почти бесплатна
В [первичном разведочном
замере](../benchmarks/2026-08-12-diarization-feasibility.md) смена порога не
изменила время: 153–157 с, а кластеризация занимала доли секунды. Минутный профиль
текущей production-конфигурации также показал `0.000 s` для clustering. Явное
число участников полезно для качества и стабильности количества кластеров, но не
является существенной оптимизацией CPU.
## Что показывают локальные измерения
| Среда и материал | Конфигурация | Результат |
|---|---|---:|
| Ryzen 7 8845H, Data Test 26:00 | Pyannote FP32 + WeSpeaker, 8 потоков | 140,7 с; 11,1× realtime |
| Ryzen 7 8845H, три 5-минутных фрагмента | WeSpeaker | средний RTF 0,118 |
| Ryzen 7 8845H, те же фрагменты | CAMPPlus zh/en | средний RTF 0,083; 0,70× от WeSpeaker |
| Intel Core i7-6820HQ, 61:13 аудио | WeSpeaker, 8 потоков | 832,3 с; 4,4× realtime |
| Текущий ноутбук, 60 с T2 BDMA | WeSpeaker, 8 потоков, debug profile | segmentation 1,243 с; embeddings 5,150 с; clustering 0,000 с; всего 6,395 с |
Источники полных воспроизводимых замеров: [разведка на
Ryzen](../benchmarks/2026-08-12-diarization-feasibility.md), [калибровка
эмбеддингов](../benchmarks/2026-08-14-diarization-calibration.md) и [Intel
baseline](../benchmarks/2026-08-14-diarization-intel-i7.md). Минутный профиль —
диагностический одиночный прогон той же production-конфигурации на первых 60
секундах T2 BDMA; его следует использовать для распределения стоимости по
стадиям, а не как новый общий benchmark.
Увеличение `num_threads` не решает проблему. На Data Test переход с четырёх
потоков (154 с) на восемь (141 с) дал только 9%. Это согласуется с устройством
конвейера: он выполняет много последовательных ONNX-вызовов для отдельных окон
и локальных говорящих, поэтому добавление потоков внутри одного вызова быстро
перестаёт масштабироваться.
## Официально поддерживаемые облегчённые варианты
### INT8 Pyannote segmentation 3.0
Sherpa-onnx официально поставляет FP32 и INT8 варианты одной сегментации. На его
контрольной записи замена только segmentation-модели при 3D-Speaker embedding
снизила RTF с 0,297 до 0,241, то есть примерно на 19%. С TitaNet small разница
меньше: 0,119 против 0,110. Все числа опубликованы на одной странице
[официальных примеров и
замеров](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/models.html).
Trade-off: это минимальное архитектурное изменение, но INT8 меняет границы
интервалов даже в официальном примере. Перед заменой нужно повторить на трёх
контрольных записях число кластеров, purity, малые кластеры, неназначенные слова
и полный runtime. На текущем минутном профиле segmentation занимает лишь около
19% времени, поэтому одной квантизацией нельзя ожидать кратного ускорения всей
диаризации.
### 3D-Speaker CAMPPlus zh/en
Модель уже входит в официальный релиз speaker-recognition моделей sherpa-onnx,
а 3D-Speaker публикует CAM++ как штатную архитектуру своего набора
([репозиторий и таблица
моделей](https://github.com/modelscope/3D-Speaker)). Её ONNX-файл занимает
28,3 МБ — немного больше текущих 26,5 МБ, поэтому размер файла здесь плохо
предсказывает вычислительную стоимость.
Локально CAMPPlus дала RTF 0,083 против 0,118 у WeSpeaker, то есть была примерно
на 30% быстрее. При известном числе участников она также улучшила proxy-purity
на двух разговорах. Но в автоматическом режиме не нашлось единого порога: один
порог оставлял лишние кластеры, следующий уже склеивал реальные голоса. Полные
данные находятся в [отчёте о
калибровке](../benchmarks/2026-08-14-diarization-calibration.md).
Trade-off: хороший кандидат для режима с обязательным `--speakers N`, но не
готовая замена общего автоматического режима.
### NeMo TitaNet small
Sherpa-onnx официально показывает `nemo_en_titanet_small` как совместимый
embedding extractor. На его контрольной записи Pyannote FP32 + TitaNet small
дала RTF 0,119 вместо 0,297 у Pyannote FP32 + 3D-Speaker ERes2Net; с INT8
сегментацией — 0,110 вместо 0,241. Это самый большой опубликованный upstream
выигрыш среди проверенных на одной странице комбинаций
([официальные замеры](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/models.html)).
TitaNet использует 1D depth-wise separable convolutions и channel-attention
statistics pooling
([документация NVIDIA](https://docs.nvidia.com/nemo-framework/user-guide/25.02/nemotoolkit/asr/speaker_recognition/models.html)).
Trade-off: upstream-цифры сняты на другой embedding-модели сравнения, другом
материале и оборудовании, поэтому коэффициент нельзя переносить на этот ноутбук.
Кроме того, TitaNet small не проходила локальную калибровку на русской речи:
неизвестны подходящий clustering threshold, стабильность числа голосов и
качество коротких реплик. Сначала нужен тот же свип, который уже выполнен для
WeSpeaker и CAMPPlus.
### Увеличение шага окна
В C++-конфигурации sherpa-onnx 1.13.5 есть `window_shift_ratio`, поэтому на уровне
движка можно уменьшить число перекрывающихся окон ценой более грубой разметки.
Но [Python binding этой
версии](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/python/csrc/offline-speaker-diarization.cc)
экспортирует только путь `model`, а не `window_shift_ratio`. Для текущего Python
приложения это не штатная ручка без изменения upstream binding или собственного
нативного слоя. Даже после появления ручки потребуется отдельная калибровка:
более редкие окна могут ухудшить границы смены голоса и короткие ответы — как раз
самую чувствительную часть текущего результата.
## Практические следующие шаги
1. Не считать текущую стоимость дефектом реализации: выбранный результат
соответствует устройству sherpa-onnx и остаётся быстрее realtime даже на
старом Intel.
2. Первым отдельным экспериментом прогнать INT8 Pyannote на тех же трёх файлах.
Это наименьший по масштабу вариант, хотя ожидаемый выигрыш умеренный.
3. Отдельно откалибровать TitaNet small. У неё лучший опубликованный потенциал
скорости, но пока нет локальных данных о качестве.
4. CAMPPlus предлагать только как кандидат для режима с известным числом
участников, если 30% экономии оправдывает второй production-профиль.
5. Параллельный ASR и diarization исследовать независимо от выбора модели. Он не
уменьшает CPU-работу, но может сократить wall-clock latency. Обязательно
измерить конкуренцию за ядра и общий peak RSS: на Intel последовательные пики
составляли 900–1035 МБ для ASR и 366–469 МБ для диаризации, а совместный пик
пока не измерен.
## Вердикт
Текущая диаризация тяжёлая **в основном из-за каскада и плотного перекрытия
окон**, а не потому, что случайно выбрана гигантская модель. Но выбранный
WeSpeaker ResNet34 LM не самый быстрый extractor. Реалистичный резерв — умеренное
ускорение через INT8 segmentation, около 30% по локальным данным через CAMPPlus
при известном числе участников и потенциально более крупное ускорение через
TitaNet small после обязательной русскоязычной калибровки. Простое добавление
потоков или настройка clustering заметного выигрыша не даст.