From bfabf09c83311f0133dd62e3dbe85fd26a6623b0 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 30 Aug 2026 13:46:21 +0300 Subject: [PATCH 1/6] =?UTF-8?q?docs(research):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=B8=D1=81=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=D0=B4=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20embedding-=D0=B8?= =?UTF-8?q?=D0=BD=D1=84=D0=B5=D1=80=D0=B5=D0=BD=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - нужно выбрать реалистичный способ ускорения эмбеддингов на разных x86 CPU. - Что: - зафиксированы ограничения batch, stream/session и static INT8 для трех моделей. - рекомендованы два прототипа и минимальная матрица CPU/ISA. - Проверка: - git diff --cached --check. --- ...6-08-30-diarization-embedding-inference.md | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 docs/research/2026-08-30-diarization-embedding-inference.md diff --git a/docs/research/2026-08-30-diarization-embedding-inference.md b/docs/research/2026-08-30-diarization-embedding-inference.md new file mode 100644 index 0000000..4d7fccc --- /dev/null +++ b/docs/research/2026-08-30-diarization-embedding-inference.md @@ -0,0 +1,208 @@ +# Ускорение извлечения голосовых эмбеддингов на x86 CPU + +**Дата:** 2026-08-30 + +**Статус:** исследовательская записка. Production-код и конфигурация не +менялись. + +**Метод:** проверены `sherpa-onnx==1.13.5`, `onnxruntime==1.28.0`, официальные +скрипты экспорта и исходники владельцев моделей. ONNX-модели не скачивались. +Формы ниже подтверждены экспортными контрактами, проверками метаданных и кодом +загрузчика. Файлы из выпуска повторно не разбирались. + +## Вопрос и краткий ответ + +Текущий путь не использует 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 +([адаптер проекта](../../src/local_transcriber/speaker_diarizer.py)). Внутри +sherpa-onnx стадии идут строго `segmentation → embeddings → clustering`, а +`ComputeEmbeddings` проходит список пар обычным циклом: создает stream, +добавляет все неперекрывающиеся фрагменты одного локального говорящего, +завершает input и синхронно вызывает `Compute` +([реализация Pyannote в sherpa-onnx 1.13.5](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h)). + +Публичный C++ и Python API экспортирует `create_stream`, `is_ready` и +`compute(stream)`. Метода `compute_batch(streams)` нет +([C++ API](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/speaker-embedding-extractor.h), +[Python binding](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/python/csrc/speaker-embedding-extractor.cc)). +Python binding освобождает GIL на `compute`, поэтому внешний параллельный вызов +в принципе возможен; готовый диаризатор промежуточные stream наружу не отдает. + +Для embedding создается одна ORT session. Sherpa-onnx передает заданное число и +в `intra_op_num_threads`, и в `inter_op_num_threads` +([настройка session](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/session.cc)). +ORT по умолчанию исполняет граф последовательно; `inter_op` начинает +распараллеливать узлы только при `ORT_PARALLEL`, тогда как `intra_op` +распараллеливает оператор +([официальная документация ORT](https://onnxruntime.ai/docs/performance/tune-performance/threading.html)). +Значит, в текущем 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](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/scripts/wespeaker/add_meta_data.py), [экспорт WeSpeaker](https://github.com/wenet-e2e/wespeaker/blob/master/wespeaker/bin/export_onnx.py)). | Равные `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](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/scripts/3dspeaker/export-onnx.py)). | В скрипте есть ошибка: выход называется `embedding`, а dynamic axis задан для `embeddings`. Поэтому динамичность оси выхода файла из выпуска по исходнику не доказана. Upstream подтверждает лишь, что batch равной длины *должен* работать; для разных длин нужна маска ([ответ разработчика](https://github.com/k2-fsa/sherpa-onnx/issues/780)). | +| NeMo TitaNet small | Два входа: `float32 [N, C, T]` и `int64 [N]` с длинами; embedding имеет форму `[N, D]` ([контракт C++-обертки](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/speaker-embedding-extractor-nemo-model.h)). Метаданные содержат `framework=nemo`, `output_dim`, `feat_dim`, sample rate, параметры окна и нормализации ([экспорт](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/scripts/nemo/speaker-verification/export-onnx.py)). | Граф имеет нужный контракт для batch переменной длины с дополнением. Но sherpa-onnx создает обе входные формы с `N=1` и возвращает только первую строку ([реализация](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/speaker-embedding-extractor-nemo-impl.h)). | + +WeSpeaker ResNet34 состоит из Conv2d-блоков и statistics pooling +([исходник WeSpeaker](https://github.com/wenet-e2e/wespeaker/blob/master/wespeaker/models/resnet.py)); +CAMPPlus сочетает Conv2d, TDNN/Conv1d и statistics pooling +([исходник 3D-Speaker](https://github.com/modelscope/3D-Speaker/blob/main/speakerlab/models/campplus/DTDNN.py)); +TitaNet использует depth-wise separable Conv1d, squeeze-and-excitation и +attention statistics pooling +([документация NeMo](https://docs.nvidia.com/nemo-framework/user-guide/25.02/nemotoolkit/asr/speaker_recognition/models.html)). +Этого недостаточно, чтобы утверждать: каждый оператор будет квантизован и +ускорен. + +В [официальном выпуске speaker-recognition models](https://github.com/k2-fsa/sherpa-onnx/releases/tag/speaker-recongition-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](https://onnxruntime.ai/docs/reference/high-level-design.html), [контракт 1.28.0](https://github.com/microsoft/onnxruntime/blob/v1.28.0/onnxruntime/core/session/inference_session.h)). Текущий цикл sherpa-onnx этого не делает. | Достойно узкого прототипа на WeSpeaker. У одной session общий intra-op pool; нужно сравнить внутренний и внешний параллелизм. Полный pool для каждого worker не нужен. | +| Несколько session | Можно создать несколько extractor, но каждая session получает собственные пулы потоков и арены памяти. ORT прямо отмечает эти накладные расходы для двух session ([ответ сопровождающего](https://github.com/microsoft/onnxruntime/issues/11628)). | Не брать как самостоятельный кандидат. Допустима одна контрольная ячейка против shared-session, чтобы подтвердить отказ по реальному времени и RSS. | +| Статический INT8 | ORT предоставляет `quantize_static`, использует calibration data и рекомендует static quantization для CNN. Для CPU первым выбором служит S8S8 QDQ; квантизация может ухудшить точность и даже скорость ([официальное руководство](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html)). | Главный кандидат для действующего 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](https://github.com/microsoft/onnxruntime/blob/v1.28.0/onnxruntime/core/mlas/lib/platform.cpp)). +Один QDQ-граф поэтому нужно измерять на разных CPU. Отдельная сборка под каждый +из них не нужна. + +Официальное руководство ORT предупреждает: + +- на AVX2 и AVX-512 без VNNI путь U8S8 использует `VPMADDUBSW` и может дать + saturation; при потере точности предусмотрены `reduce_range` или U8U8; +- на x86 с VNNI этой проблемы нет, а INT8 обычно выигрывает больше; +- на старом CPU без подходящих инструкций quantize/dequantize overhead способен + сделать модель медленнее + ([разделы Data type selection и FAQ](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html)). + +Доступный Core i7-6820HQ официально заявляет только SSE4.1/4.2 и AVX2 +([Intel ARK](https://www.intel.com/content/www/us/en/products/sku/88970/intel-core-i76820hq-processor-8m-cache-up-to-3-60-ghz/specifications.html)). +Современный класс должен выбираться по фактическим CPUID-флагам. Надписи +"11-е поколение" недостаточно: минимум нужен `avx512_vnni` или `avx_vnni`. Для доступного +Zen 4 AMD документирует AVX-512 и AVX512_VNNI как путь ускоренного integer GEMM +([AMD AOCL](https://docs.amd.com/r/en-US/68552-AOCL-api-guide/Hardware-Utilization)). + +В параллельном прототипе нужно измерять RSS. Уже последовательный тест показал, +что TitaNet требует примерно на 147–149 МиБ больше RSS, чем WeSpeaker, а FP32 +WeSpeaker на старом Intel использовал в среднем 6,84 логического ядра из восьми +([локальный benchmark](../benchmarks/2026-08-30-lightweight-diarization-cpu.md)). +Конкурентные `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 и сравнить: + +```text +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 нельзя. -- 2.54.0 From 5c6697ecbf16fb6a9d24e5261482811d19b94c13 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 30 Aug 2026 19:19:28 +0300 Subject: [PATCH 2/6] =?UTF-8?q?docs(research):=20=D1=83=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=8B=20batch-=D0=BA=D0=BE=D0=BD=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D1=8B=20embedding-=D0=BC=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - вопрос тикета требует проверить формы официальных release-графов, а исходная записка опиралась только на экспортные контракты. - Что: - зафиксированы реальные формы и CPU smoke-test batch size 2 для WeSpeaker, CAMPPlus и TitaNet. - уточнены поддержка equal-length batch у CAMPPlus и fallback между S8S8, U8U8 и U8S8. - Проверка: - git diff --check. --- ...6-08-30-diarization-embedding-inference.md | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/docs/research/2026-08-30-diarization-embedding-inference.md b/docs/research/2026-08-30-diarization-embedding-inference.md index 4d7fccc..fafdff6 100644 --- a/docs/research/2026-08-30-diarization-embedding-inference.md +++ b/docs/research/2026-08-30-diarization-embedding-inference.md @@ -6,9 +6,10 @@ менялись. **Метод:** проверены `sherpa-onnx==1.13.5`, `onnxruntime==1.28.0`, официальные -скрипты экспорта и исходники владельцев моделей. ONNX-модели не скачивались. -Формы ниже подтверждены экспортными контрактами, проверками метаданных и кодом -загрузчика. Файлы из выпуска повторно не разбирались. +скрипты экспорта и исходники владельцев моделей. Три официальных release asset +прочитаны потоком без сохранения на диск: WeSpeaker ResNet34 LM, CAMPPlus en и +TitaNet small. Их входы, выходы и метаданные разобраны через ORT 1.28.0; для +каждого выполнен CPU smoke-test с batch size 2. ## Вопрос и краткий ответ @@ -64,9 +65,18 @@ ORT по умолчанию исполняет граф последовател | Модель | Вход и выход официального экспорта | Следствие для 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](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/scripts/wespeaker/add_meta_data.py), [экспорт WeSpeaker](https://github.com/wenet-e2e/wespeaker/blob/master/wespeaker/bin/export_onnx.py)). | Равные `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](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/scripts/3dspeaker/export-onnx.py)). | В скрипте есть ошибка: выход называется `embedding`, а dynamic axis задан для `embeddings`. Поэтому динамичность оси выхода файла из выпуска по исходнику не доказана. Upstream подтверждает лишь, что batch равной длины *должен* работать; для разных длин нужна маска ([ответ разработчика](https://github.com/k2-fsa/sherpa-onnx/issues/780)). | -| NeMo TitaNet small | Два входа: `float32 [N, C, T]` и `int64 [N]` с длинами; embedding имеет форму `[N, D]` ([контракт C++-обертки](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/speaker-embedding-extractor-nemo-model.h)). Метаданные содержат `framework=nemo`, `output_dim`, `feat_dim`, sample rate, параметры окна и нормализации ([экспорт](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/scripts/nemo/speaker-verification/export-onnx.py)). | Граф имеет нужный контракт для batch переменной длины с дополнением. Но sherpa-onnx создает обе входные формы с `N=1` и возвращает только первую строку ([реализация](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/speaker-embedding-extractor-nemo-impl.h)). | +| WeSpeaker ResNet34 LM | Release asset имеет один `float32`-вход `[B, T, 80]` и выход `[B, 256]` ([asset](https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/wespeaker_en_voxceleb_resnet34_LM.onnx)). Скрипт, которым sherpa-onnx добавляет метаданные, проверяет символические `B`, `T`, размерность 80 и единственный выход. Метаданные включают `framework=wespeaker`, `sample_rate`, `output_dim`, `normalize_samples=0` ([проверка sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/scripts/wespeaker/add_meta_data.py), [экспорт WeSpeaker](https://github.com/wenet-e2e/wespeaker/blob/master/wespeaker/bin/export_onnx.py)). | Равные `T` можно сложить по `B`. Длины или маску граф не принимает; дополнение участвует в pooling и меняет эмбеддинг. | +| CAMPPlus en | Release asset имеет `float32`-вход `[N, T, 80]` и выход с динамической первой осью `[*, 512]` ([asset](https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_campplus_sv_en_voxceleb_16k.onnx)). Метаданные: `framework=3d-speaker`, `output_dim=512`, `normalize_samples=1`, `feature_normalize_type=global-mean` ([экспорт sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/scripts/3dspeaker/export-onnx.py)). | Несмотря на опечатку экспортного скрипта (`embedding`/`embeddings`), release asset принимает batch равной длины и возвращает все строки. Для разных длин граф по-прежнему не принимает маску или длины. | +| NeMo TitaNet small | Release asset имеет входы `float32 [N, 80, T]` и `int64 [N]`; выходы — logits `[N, 16681]` и embedding `[N, 192]` ([asset](https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/nemo_en_titanet_small.onnx), [контракт C++-обертки](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/speaker-embedding-extractor-nemo-model.h)). Метаданные содержат `framework=nemo`, `output_dim`, `feat_dim`, sample rate, параметры окна и нормализации ([экспорт](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/scripts/nemo/speaker-verification/export-onnx.py)). | Граф принимает batch и вектор длин. Но sherpa-onnx создает обе входные формы с `N=1` и возвращает только первую строку ([реализация](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/speaker-embedding-extractor-nemo-impl.h)). | + +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](https://github.com/wenet-e2e/wespeaker/blob/master/wespeaker/models/resnet.py)); @@ -87,9 +97,9 @@ assets нет. Статический INT8 здесь означает собс | Способ | Техническая доступность | Вердикт | |---|---|---| -| Настоящий batch | Граф WeSpeaker поддерживает равные длины; CAMPPlus требует прямой проверки файла из выпуска; TitaNet поддерживает разные длины через второй вход. Sherpa-onnx для всех трех жестко формирует `N=1` и не имеет batch API. | Прототипировать только TitaNet и только условно для режима с известным числом говорящих. Для WeSpeaker/CAMPPlus без маски возможны лишь группы с одинаковым `T`; ожидаемая наполняемость неизвестна. | +| Настоящий 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](https://onnxruntime.ai/docs/reference/high-level-design.html), [контракт 1.28.0](https://github.com/microsoft/onnxruntime/blob/v1.28.0/onnxruntime/core/session/inference_session.h)). Текущий цикл sherpa-onnx этого не делает. | Достойно узкого прототипа на WeSpeaker. У одной session общий intra-op pool; нужно сравнить внутренний и внешний параллелизм. Полный pool для каждого worker не нужен. | -| Несколько session | Можно создать несколько extractor, но каждая session получает собственные пулы потоков и арены памяти. ORT прямо отмечает эти накладные расходы для двух session ([ответ сопровождающего](https://github.com/microsoft/onnxruntime/issues/11628)). | Не брать как самостоятельный кандидат. Допустима одна контрольная ячейка против shared-session, чтобы подтвердить отказ по реальному времени и RSS. | +| Несколько session | Можно создать несколько extractor, но по умолчанию каждая session получает собственный intra-op pool и CPU arena. ORT отдельно предлагает общий pool против конкуренции session pools и общий allocator против роста памяти ([thread management](https://onnxruntime.ai/docs/performance/tune-performance/threading.html), [C API guide](https://onnxruntime.ai/docs/get-started/with-c.html#share-allocator-s-between-sessions)). | Не брать как самостоятельный кандидат. Допустима одна контрольная ячейка против shared-session, чтобы подтвердить отказ по реальному времени и RSS. | | Статический INT8 | ORT предоставляет `quantize_static`, использует calibration data и рекомендует static quantization для CNN. Для CPU первым выбором служит S8S8 QDQ; квантизация может ухудшить точность и даже скорость ([официальное руководство](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html)). | Главный кандидат для действующего WeSpeaker. CAMPPlus и TitaNet квантизовать только после продуктового решения использовать соответствующую модель. | Batch сокращает число `Session::Run` и может лучше загрузить SIMD, но число @@ -111,8 +121,11 @@ AVX512-VNNI kernels Официальное руководство ORT предупреждает: -- на AVX2 и AVX-512 без VNNI путь U8S8 использует `VPMADDUBSW` и может дать - saturation; при потере точности предусмотрены `reduce_range` или U8U8; +- S8S8 QDQ остается первым выбором CPU. Если он дает значимую потерю + точности, ORT советует проверить U8U8; +- отдельный путь U8S8 на AVX2 и AVX-512 без VNNI использует `VPMADDUBSW` и + может дать saturation; если до него дойдет матрица, этот риск снижают + `reduce_range` или переход на U8U8; - на x86 с VNNI этой проблемы нет, а INT8 обычно выигрывает больше; - на старом CPU без подходящих инструкций quantize/dequantize overhead способен сделать модель медленнее @@ -143,8 +156,6 @@ WeSpeaker на старом Intel использовал в среднем 6,84 не загрузит. - Точность статического INT8 после калибровки и необходимость исключить отдельные узлы через quantization debugging. -- Фактическая batch-форма выхода CAMPPlus release asset из-за ошибки в экспортном - скрипте. - Распределение `T` у пар "окно, локальный говорящий". Без него нельзя оценить полезность exact-length buckets для WeSpeaker/CAMPPlus. - Выигрыш shared-session concurrency после ограничения общего числа потоков, @@ -165,8 +176,8 @@ WeSpeaker на старом Intel использовал в среднем 6,84 - только после технического выигрыша повторить действующие проверки числа голосовых кластеров, purity и слов без говорящего. -U8U8 или `reduce_range` добавлять лишь при подтвержденной потере точности на -AVX2. В первую матрицу эти варианты не входят. +При значительной потере точности S8S8 проверить U8U8. Отдельный U8S8 с +`reduce_range` в первую матрицу не входит. ### 2. Shared-session streams - второй кандидат -- 2.54.0 From b09036eaa3c67b14d7046720b8a044026dbd2d12 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 30 Aug 2026 13:42:42 +0300 Subject: [PATCH 3/6] =?UTF-8?q?docs(research):=20=D0=B2=D1=8B=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=86=D0=B5=D0=BD=D0=BA=D0=B8=20=D1=87=D0=B8=D1=81=D0=BB=D0=B0?= =?UTF-8?q?=20=D0=B3=D0=BE=D0=B2=D0=BE=D1=80=D1=8F=D1=89=D0=B8=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - быстрым моделям эмбеддингов нужен автоматический подсчет без повторного инференса. - Что: - проверены возможности повторного использования эмбеддингов в публичном API sherpa-onnx. - сопоставлены порог, eigengap, NME-SC и двухшаговая кластеризация. - зафиксирована граница NumPy-прототипа без production-зависимости от NeMo. - Проверка: - git diff --cached --check. --- .../2026-08-30-diarization-speaker-count.md | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 docs/research/2026-08-30-diarization-speaker-count.md diff --git a/docs/research/2026-08-30-diarization-speaker-count.md b/docs/research/2026-08-30-diarization-speaker-count.md new file mode 100644 index 0000000..4fe3bb0 --- /dev/null +++ b/docs/research/2026-08-30-diarization-speaker-count.md @@ -0,0 +1,281 @@ +# Оценка числа говорящих для быстрых эмбеддингов + +**Дата:** 2026-08-30 + +**Статус:** исследовательская записка. Production-код и конфигурация не +менялись. + +## Вопрос + +Как отделить оценку числа говорящих от порога `FastClustering`, чтобы +использовать быстрые CAMPPlus или TitaNet и не запускать сегментацию и +извлечение эмбеддингов второй раз? + +## Краткий вывод + +Нужна двухшаговая схема: один раз получить ровно те эмбеддинги, которые +подготовил конвейер sherpa-onnx, отдельно оценить по ним число голосовых +кластеров `N`, затем передать `N` штатному `FastClustering`. Для первого +прототипа рекомендован **только счетчик NME**, без полной spectral clustering: +ядро алгоритма из статьи реализовать на уже доступном NumPy, а итоговые метки +по-прежнему получить через `FastClustering(num_clusters=N)`. Простой eigengap с +фиксированным прореживанием нужен как дешевый контрольный вариант. + +Публичного промежуточного интерфейса для этого в sherpa-onnx 1.13.5 нет. +`SpeakerEmbeddingExtractor` и `FastClustering` публичны, но +`OfflineSpeakerDiarization.process()` возвращает только готовые интервалы. +Матрица эмбеддингов и соответствие строк локальным говорящим остаются внутри +реализации. После `set_config()` повторный `process()` снова выполнит модельный +инференс. Python-адаптер проекта сам по себе не может дать дешевую повторную +кластеризацию; будущему прототипу нужен узкий исследовательский интерфейс на +уровне sherpa-onnx. + +## Что подтверждено про sherpa-onnx + +### `FastClustering` уже умеет второй шаг + +В версии 1.13.5 алгоритм нормирует строки, строит все попарные cosine +dissimilarity, выполняет complete-linkage AHC и затем выбирает один из двух +способов разрезать одно и то же дерево: + +- при `num_clusters > 0` - ровно на заданное число кластеров; +- иначе - по высоте `threshold`. + +Это видно непосредственно в +[`fast-clustering.cc`](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/fast-clustering.cc) +и закреплено в комментарии к +[`FastClusteringConfig`](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/fast-clustering-config.h): +при известном числе кластеров порог игнорируется. Python binding публично +принимает C-contiguous матрицу `float32` размера `M x D` и возвращает метку для +каждой строки +([исходник binding](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/python/csrc/fast-clustering.cc)). +Значит, передавать оцененное `N` в существующий кластеризатор не требует новой +реализации распределения эмбеддингов по кластерам. + +Порог `FastClustering` не принадлежит модели эмбеддингов. Это высота +complete-linkage дерева в шкале cosine dissimilarity. Официальный пример +CAMPPlus для speaker verification лишь вычисляет cosine score и не задает +универсальной границы решения +([`infer_sv.py`](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/speakerlab/bin/infer_sv.py)). +Официальный diarization-рецепт 3D-Speaker отдельно задает `pval=0.012` для +spectral clustering и `mer_cos=0.8` для последующего слияния центров +([`infer_diarization.py`](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/speakerlab/bin/infer_diarization.py)). +Эти величины имеют другой смысл и не могут быть перенесены в sherpa как +`порог модели`. + +### Публичных деталей недостаточно для повтора всего конвейера + +Публичный `SpeakerEmbeddingExtractor` позволяет создать stream, подать в него +аудио и получить один embedding +([Python binding](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/python/csrc/speaker-embedding-extractor.cc), +[C/C++ API](https://k2-fsa.github.io/sherpa/onnx/c-api/html/speaker_embedding.html)). +Это пригодно для самостоятельно выбранного фрагмента, но не выдает материал, +который выбрал diarization-конвейер. + +Внутри `OfflineSpeakerDiarization` после сегментации: + +1. из перекрывающихся окон строятся пары `окно x локальный говорящий`; +2. речь с одновременными голосами исключается; +3. для каждой пары собирается свой stream и вычисляется embedding; +4. вся матрица передается в `FastClustering`; +5. метки строк переводятся обратно в разметку говорящих. + +Последовательность и временная матрица видны в +[`OfflineSpeakerDiarizationPyannoteImpl::Process`](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h#L87-L204), +а сборка строк - в +[`GetChunkSpeakerSampleIndexes` и `ComputeEmbeddings`](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h#L376-L528). +Это private-методы реализации. + +Python-класс `OfflineSpeakerDiarization` экспортирует только `sample_rate`, +`set_config` и `process`; `process` возвращает готовый результат, а не признаки +([binding 1.13.5](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/python/csrc/offline-speaker-diarization.cc#L73-L128)). +Внутренний `SetConfig` заменяет только объект кластеризации, тогда как каждый +`Process` заново вызывает segmentation и `ComputeEmbeddings` +([реализация](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h#L79-L175)). +Актуальная публичная C API также документирует только создание, настройку и +получение итоговых сегментов +([официальная страница](https://k2-fsa.github.io/sherpa/onnx/c-api/html/speaker_diarization.html)). + +Итог: публичный API позволяет **кластеризовать имеющуюся матрицу повторно**. +Извлечь или переиграть промежуточную матрицу штатного diarization-конвейера +через него нельзя. + +## Варианты оценки `N` + +### 1. Отдельный порог для каждой embedding-модели + +Это текущий `FastClustering` в автоматическом режиме. Он дешев, не добавляет +зависимостей и на одном проходе сразу оценивает число кластеров и назначает +метки. На порог влияют архитектура модели эмбеддингов, длительность и состав +выбранных фрагментов, акустика и распределение голосов. Локальная калибровка +уже не нашла общего рабочего порога для CAMPPlus, +а TitaNet не прошел автоматическую сетку +([исходное исследование](2026-08-14-diarization-cpu-cost.md), +[повторный CPU-тест](../benchmarks/2026-08-30-lightweight-diarization-cpu.md)). + +Вводить еще один `официальный порог модели` оснований нет: upstream-рецепты +публикуют параметры конкретного clustering pipeline, а не переносимую границу +для complete-linkage sherpa. Вариант остается контрольным, но не решает тикет. + +### 2. Spectral clustering с простым eigengap + +Официальный CAMPPlus-конвейер 3D-Speaker строит cosine affinity, оставляет в +каждой строке фиксированную долю ближайших соседей, симметризует матрицу, +вычисляет ненормированный Laplacian и несколько наименьших собственных пар. +Число говорящих - позиция максимального eigengap в заданных пределах, после +чего собственные векторы кластеризуются k-means +([`SpectralCluster`](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/speakerlab/process/cluster.py#L21-L106)). + +Для нашей двухшаговой схемы нужны только собственные значения и оценка `N`; +k-means можно не выполнять. Это одна spectral decomposition и потому хороший +минимальный контрольный вариант. Однако фиксированный `pval` остается новым +порогом. +Официальная реализация задает его для своего способа нарезки; пары +`окно x локальный говорящий` sherpa устроены иначе. Вариант отделяет `N` от +порога AHC, но сохраняет калибруемый параметр. + +Если брать реализацию 3D-Speaker как зависимость, она импортирует NumPy, SciPy, +scikit-learn, fastcluster, UMAP и HDBSCAN, причем последние два обязательны уже +при импорте модуля +([исходник](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/speakerlab/process/cluster.py#L3-L18), +[requirements](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/requirements.txt)). +Для production это несоразмерно счетчику. + +### 3. NME-SC + +NME-SC создан именно для совместной оценки числа кластеров и параметра +прореживания без dev-set. Для каждого числа соседей `p` алгоритм бинаризует +cosine affinity, симметризует ее, строит ненормированный Laplacian и считает +eigengap. Нормированный gap равен максимальному gap, деленному на наибольшее +собственное значение; выбирается `p`, минимизирующий отношение `p / gap`, а `N` +задает позиция максимального gap +([статья авторов, раздел III](https://arxiv.org/abs/2003.02405)). +Авторы ограничивали поиск `p` диапазоном до четверти числа фрагментов и число +говорящих - восемью; результаты статьи получены на x-vector и англоязычных +корпусах, поэтому перенос на наши эмбеддинги еще предстоит проверить. + +Поддерживаемая реализация NeMo принимает готовые эмбеддинги, строит cosine +affinity и отдельно получает `est_num_of_spk`; известное число говорящих затем +подменяет эту оценку перед spectral clustering +([`offline_clustering.py`](https://github.com/NVIDIA-NeMo/Speech/blob/d47d7a3c306790637aff39c2d3f9eceb83ca8879/nemo/collections/asr/parts/utils/offline_clustering.py#L1087-L1161)). +Для ускорения NME она по умолчанию ограничивает матрицу анализа 512 строками, +перебирает разреженную сетку из 30 значений `p` и предупреждает, что меньше 20 +может ухудшить оценку +([класс `NMESC`](https://github.com/NVIDIA-NeMo/Speech/blob/d47d7a3c306790637aff39c2d3f9eceb83ca8879/nemo/collections/asr/parts/utils/offline_clustering.py#L801-L958)). +Для коротких записей NeMo добавляет случайные anchor embeddings и повторяет +оценку. Это отдельное расширение за пределами минимального ядра статьи +([исходник](https://github.com/NVIDIA-NeMo/Speech/blob/d47d7a3c306790637aff39c2d3f9eceb83ca8879/nemo/collections/asr/parts/utils/offline_clustering.py#L536-L595)). + +Полная NME-SC после оценки `N` еще раз раскладывает Laplacian, строит spectral +embeddings и запускает k-means. Для этого тикета такая замена итогового +кластеризатора не нужна: локальные тесты уже показывают полезный путь +`FastClustering` с известным числом говорящих. Нужна только функция +`embeddings -> N`. + +### 4. Оценить `N`, затем передать известное число кластеров + +Это не отдельный счетчик. Это рекомендуемая граница ответственности: + +```text +segmentation -> embeddings +-> count-only NME -> N + +-> FastClustering(features, num_clusters=N) + -> штатная финализация sherpa +``` + +Плюсы: + +- порог complete-linkage больше не определяет число голосовых кластеров; +- CAMPPlus и TitaNet используют один модельный проход; +- итоговое назначение меток остается в уже принятом FastClustering; +- на одной матрице можно независимо сравнить несколько счетчиков. + +Минус один, но архитектурный: без нового промежуточного интерфейса текущий +`process()` не позволяет вставить счетчик между вычислением матрицы и +кластеризацией. + +## Зависимости, лицензии и цена + +Штатная установка NeMo требует среди прочего NumPy, scikit-learn и PyTorch +([`pyproject.toml`](https://github.com/NVIDIA-NeMo/Speech/blob/d47d7a3c306790637aff39c2d3f9eceb83ca8879/pyproject.toml)). + +| Вариант | Новые runtime-зависимости | Дополнительная работа после эмбеддингов | Ограничение | +|---|---|---|---| +| Порог `FastClustering` | нет | уже существующие попарные расстояния и AHC | порог калибруется для модели и материала | +| Простой eigengap | SciPy в официальной реализации; либо только уже доступный NumPy | affinity `M x M`, одно собственное разложение | фиксированный параметр прореживания | +| Полная NME-SC NeMo | `nemo-toolkit`, PyTorch, scikit-learn и другие зависимости NeMo | до нескольких десятков eigenvalue-разложений, затем еще spectral embedding и k-means | самый тяжелый и меняет итоговый кластеризатор | +| Счетчик NME + FastClustering | только NumPy при чистой реализации ядра статьи | NME на ограниченной матрице, затем штатный AHC по известному `N` | нужен промежуточный интерфейс и локальная проверка переноса | + +`FastClustering` уже хранит `M(M-1)/2` расстояний и строит AHC. Простой +eigengap добавляет еще одну плотную affinity-матрицу. NME повторяет eigenvalue- +анализ для набора `p`; ограничение размера и разреженный поиск контролируют +цену, но их влияние на качество нужно измерить. Полная spectral clustering +также хранит собственные векторы и запускает k-means, поэтому для одной оценки +`N` избыточна. В проектных замерах основной расход по-прежнему приходится на +извлечение эмбеддингов, а не на clustering +([профиль стоимости](2026-08-14-diarization-cpu-cost.md), +[CPU-тест](../benchmarks/2026-08-30-lightweight-diarization-cpu.md)); это не +заменяет отдельного замера NME на целевой матрице. + +sherpa-onnx, 3D-Speaker и NeMo Speech распространяют код под Apache-2.0 +([sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/LICENSE), +[3D-Speaker](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/LICENSE), +[NeMo Speech](https://github.com/NVIDIA-NeMo/Speech/blob/d47d7a3c306790637aff39c2d3f9eceb83ca8879/LICENSE)). +Авторская реализация NME-SC опубликована под MIT и сама направляет пользователей +к поддерживаемой версии NeMo +([репозиторий авторов](https://github.com/tango4j/Auto-Tuning-Spectral-Clustering/tree/ae97f9be9c33b554205c7192c5137ce8f456c1b7)). +Файл NeMo с clustering дополнительно сохраняет BSD-уведомление scikit-learn. +NumPy имеет BSD-3-Clause +([лицензия 2.4.3](https://github.com/numpy/numpy/blob/v2.4.3/LICENSE.txt)) и уже +присутствует в [lock-файле проекта](../../uv.lock). Лицензионного запрета на +прототип нет; +прямое заимствование кода потребует сохранить его уведомления. Новые модели +для счетчика не нужны. + +## Ограничения вывода + +- NME-SC проверялась авторами на другой нарезке и x-vector. Строки sherpa - это + локальные говорящие перекрывающихся Pyannote-окон после исключения overlap; + переносимость нельзя считать доказанной. +- Правильное `N` не гарантирует качественную разметку говорящих. Для TitaNet в + режиме известного числа кластеров уже наблюдался малый кластер, поэтому нужны + прежние proxy-метрики и слуховая проверка. +- У sherpa есть отдельный путь для единственного окна, который обходится без + общей матрицы эмбеддингов. Прототип не должен ломать этот special case. +- Python binding `FastClustering` нормирует переданную матрицу на месте; + сравниваемым вариантам нужно выдавать отдельные C-contiguous копии. +- Цена NME на числе строк, которое реально создает sherpa, здесь не измерялась. + +## Точная граница будущего прототипа + +Production-код, CLI, модели по умолчанию и ADR не менять. Сделать отдельный +исследовательский стенд на закрепленном sherpa-onnx 1.13.5: + +1. Узко разделить внутренний `Process` на подготовку и финализацию либо добавить + исследовательский callback после `ComputeEmbeddings`. За один проход + сохранить в памяти матрицу `float32`, список валидных пар + `окно x локальный говорящий` и локальные frame labels; аудио и текст не + сериализовать. +2. На **одной и той же** матрице сравнить три счетчика: + текущий порог как контроль, простой eigengap с фиксированным прореживанием и + count-only NME. Ограничить `N` диапазоном `1..8`. +3. Минимальный NME реализовать по статье на NumPy: cosine affinity, + top-`p` binarization, симметризация, ненормированный Laplacian, + `numpy.linalg.eigvalsh`, выбор `p` и `N` по NME. Для первого прогона взять + детерминированное ограничение 512 строк и sparse search из 20 значений; + anchor embeddings, Torch, spectral embeddings и k-means не включать. +4. Для каждого оцененного `N` вызвать публичный `FastClustering` на копии той же + матрицы с `num_clusters=N`, затем выполнить существующую финализацию без + повторной сегментации или извлечения эмбеддингов. +5. На тех же трех контрольных фрагментах измерить: оцененное `N`, общее и + содержательное число кластеров, mapped speaker purity, слова без говорящего, + время счетчика/кластеризации и peak RSS. Прогон с CAMPPlus и TitaNet должен + завершиться слуховой проверкой. +6. Если простой eigengap и NME одинаково проходят проверку, выбрать более + дешевый eigengap. Если проходит только NME, отдельно решить, переносить ли + чистую NumPy-реализацию в production. Если оба ошибаются, быстрые embedding- + модели оставить только для явно заданного числа говорящих. + +Этот прототип отвечает ровно на два неизвестных: можно ли стабильно оценить +`N` по внутренним эмбеддингам CAMPPlus/TitaNet и укладывается ли count-only NME +в приемлемую CPU-цену. Он не выбирает новую модель по умолчанию, не заменяет +FastClustering и не расширяет продуктовый контракт. -- 2.54.0 From 2fc777fdee28cdca6dfed51475b6f95768231b94 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 30 Aug 2026 20:58:18 +0300 Subject: [PATCH 4/6] =?UTF-8?q?docs(cpu):=20=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B0=D0=BF=D0=BF=D0=B0=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BC=D0=B0=D1=82=D1=80=D0=B8=D1=86=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - Конкретный Core i5 ошибочно стал обязательной приемочной машиной при разнородном парке пользователей. - Что: - CPU-дефолт отделен от воспроизводимых Intel и AMD baseline. - README, backlog и исследование приведены к единой терминологии. - Проверка: - `git diff --check`. --- README.md | 3 +- docs/backlog.md | 35 +++++++++++++------ ...6-08-30-diarization-embedding-inference.md | 16 +++++---- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3278ee2..88a75d4 100644 --- a/README.md +++ b/README.md @@ -353,7 +353,8 @@ faster-whisper её не поддерживает, и запуск с `--device | `parakeet-v3` | ~600 MB | 7,6×* | 25 языков | ✅ | \* Наблюдение на AMD Ryzen 7 8845H, Windows, `int8`, три записи общей -длительностью 43:37. Это не приёмочный замер для целевого Intel Core i5. +длительностью 43:37. Это результат на одной из доступных тестовых машин, а не +прогноз производительности на других CPU. Методика и качественное сравнение: [benchmark GigaAM и Whisper](docs/benchmarks/2026-08-11-gigaam-model-comparison.md). diff --git a/docs/backlog.md b/docs/backlog.md index ed6450b..6f75754 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -13,7 +13,12 @@ **Промежуточный статус 2026-08-11:** короткая матрица на трёх записях завершена и описана в [benchmark GigaAM и Whisper](benchmarks/2026-08-11-gigaam-model-comparison.md). Multilingual Large добавлена как явный качественный профиль; решение о default -по-прежнему ждёт целевого Intel Core i5 и длинных негативных примеров. +по-прежнему ждёт длинных негативных примеров и проверки производительности на +нескольких доступных классах CPU. + +Под CPU-дефолтом в этом разделе понимается программный профиль: backend, модель +и `compute_type`, выбранные без явных аргументов пользователя. Целевого +процессора и обязательного эталонного SKU у проекта нет. **Проблема:** текущий CPU-путь через OpenVINO Whisper medium нестабилен на длинных записях с тихими участками: модель генерирует правдоподобные повторы и @@ -68,10 +73,15 @@ Multilingual Large добавлена как явный качественный **Матрица производительности:** -- обязательный прогон на реальном Intel Core i5 11-го поколения; точный SKU, - число ядер, объём RAM, power mode и число потоков записать вместе с результатом; -- Ryzen 7 8845H разработчика и ограничение числа потоков использовать только для - предварительного smoke-теста, не как приёмку производительности i5; +- использовать минимум два доступных x86-ориентира: консервативный baseline на + Intel Core i7-6820HQ и современный baseline на AMD Ryzen 7 8845H; +- для каждого прогона записывать точный SKU, физические ядра и логические + потоки, CPUID/ISA, объём RAM, power mode и фактическое число потоков; +- Ryzen использовать как основную машину для итераций и как современный + контролируемый baseline, а не переносить его результат на остальные CPU; +- при доступности отдельно собирать результаты на других x86-ноутбуках и Apple + Silicon как данные о переносимости. Такие внешние прогоны не заменяют + контролируемую матрицу; - измерять отдельно cold start/загрузку модели и warm transcription, медиану трёх прогонов, RTFx, peak RSS и размер скачиваемой модели; - короткий 15-минутный фрагмент нужен для итераций, полные записи 22–81 мин — @@ -92,8 +102,9 @@ Multilingual Large добавлена как явный качественный **Если подтвердится бенчмарком:** -- Если E2E RNN-T сохраняет качество и приемлемую скорость на i5 — сделать его - русским CPU-дефолтом, OpenVINO оставить явной опцией. +- Если E2E RNN-T сохраняет качество и приемлемую скорость на выбранной + CPU-матрице - сделать его русским CPU-дефолтом, OpenVINO оставить явной + опцией. - Если лучший вариант зависит от языка — выбрать language-aware default: GigaAM v3 для русского, multilingual-модель для смешанной речи. - Если модели GigaAM проигрывают по пунктуации/смыслу — сохранить текущий @@ -135,10 +146,12 @@ Multilingual Large добавлена как явный качественный - Переключить русский CPU-дефолт на победителя сравнительного GigaAM-бенчмарка. - Предупреждать пользователя при `--device openvino-cpu` для файлов >30 мин. -**Приоритет:** высокий — проблема затрагивает текущий OpenVINO CPU-путь, а -целевая аудитория включает ноутбуки с Intel Core i5 11-го поколения. GigaAM v3 -остаётся рабочей явной альтернативой для русского, но выбор безопасного -auto/default требует сравнительного бенчмарка. +**Приоритет:** высокий - проблема затрагивает текущий OpenVINO CPU-путь, а +пользователи работают на ноутбуках разных поколений и производителей. +Встречаются и машины уровня Intel Core i5 11-го поколения, но это пример +распространенной конфигурации, а не целевой SKU. GigaAM v3 остается рабочей +явной альтернативой для русского, но выбор безопасного auto/default требует +сравнительного бенчмарка. --- diff --git a/docs/research/2026-08-30-diarization-embedding-inference.md b/docs/research/2026-08-30-diarization-embedding-inference.md index fafdff6..ff14d56 100644 --- a/docs/research/2026-08-30-diarization-embedding-inference.md +++ b/docs/research/2026-08-30-diarization-embedding-inference.md @@ -206,14 +206,16 @@ inter-op threads: 1 sherpa-onnx API ради TitaNet преждевременно. CAMPPlus и batch без mask в эту границу не входят. -Минимальная CPU-матрица для обоих непосредственных кандидатов: +Минимальная доступная 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. | +| Консервативный 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. | -Фактический целевой Core i5 11-го поколения добавить как приемочную машину, -когда он появится; перед запуском записать точную модель и CPUID. Двух машин -достаточно, чтобы решить судьбу кандидата на двух ISA-классах. Переносить -полученный коэффициент ускорения на весь x86 нельзя. +Эти две машины доступны для воспроизводимых прогонов; ни одна из них не является +целевым SKU проекта. Ноутбук с Intel Core i5 11-го поколения - лишь пример +распространенного пользовательского класса и не обязательная приемочная машина. +Для первого отбора кандидата двух ориентиров достаточно; на любой дополнительной +машине нужно записывать точную модель и CPUID. Переносить полученный коэффициент +ускорения на весь x86 нельзя. -- 2.54.0 From bcf264e0aa416a3c2ee2d73aa5c9f9e211aeb01a Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 30 Aug 2026 21:04:03 +0300 Subject: [PATCH 5/6] =?UTF-8?q?docs(research):=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D1=80=D0=B5=D0=B6=D0=B4=D0=B5=D0=B2?= =?UTF-8?q?=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D0=B0=D1=8F=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - Исследование числа говорящих не проходило отдельный Wayfinder-тикет и не должно публиковаться как принятое решение. - Что: - Записка об оценке числа говорящих удалена из publication-ветки. - Проверка: - `git diff --check`. --- .../2026-08-30-diarization-speaker-count.md | 281 ------------------ 1 file changed, 281 deletions(-) delete mode 100644 docs/research/2026-08-30-diarization-speaker-count.md diff --git a/docs/research/2026-08-30-diarization-speaker-count.md b/docs/research/2026-08-30-diarization-speaker-count.md deleted file mode 100644 index 4fe3bb0..0000000 --- a/docs/research/2026-08-30-diarization-speaker-count.md +++ /dev/null @@ -1,281 +0,0 @@ -# Оценка числа говорящих для быстрых эмбеддингов - -**Дата:** 2026-08-30 - -**Статус:** исследовательская записка. Production-код и конфигурация не -менялись. - -## Вопрос - -Как отделить оценку числа говорящих от порога `FastClustering`, чтобы -использовать быстрые CAMPPlus или TitaNet и не запускать сегментацию и -извлечение эмбеддингов второй раз? - -## Краткий вывод - -Нужна двухшаговая схема: один раз получить ровно те эмбеддинги, которые -подготовил конвейер sherpa-onnx, отдельно оценить по ним число голосовых -кластеров `N`, затем передать `N` штатному `FastClustering`. Для первого -прототипа рекомендован **только счетчик NME**, без полной spectral clustering: -ядро алгоритма из статьи реализовать на уже доступном NumPy, а итоговые метки -по-прежнему получить через `FastClustering(num_clusters=N)`. Простой eigengap с -фиксированным прореживанием нужен как дешевый контрольный вариант. - -Публичного промежуточного интерфейса для этого в sherpa-onnx 1.13.5 нет. -`SpeakerEmbeddingExtractor` и `FastClustering` публичны, но -`OfflineSpeakerDiarization.process()` возвращает только готовые интервалы. -Матрица эмбеддингов и соответствие строк локальным говорящим остаются внутри -реализации. После `set_config()` повторный `process()` снова выполнит модельный -инференс. Python-адаптер проекта сам по себе не может дать дешевую повторную -кластеризацию; будущему прототипу нужен узкий исследовательский интерфейс на -уровне sherpa-onnx. - -## Что подтверждено про sherpa-onnx - -### `FastClustering` уже умеет второй шаг - -В версии 1.13.5 алгоритм нормирует строки, строит все попарные cosine -dissimilarity, выполняет complete-linkage AHC и затем выбирает один из двух -способов разрезать одно и то же дерево: - -- при `num_clusters > 0` - ровно на заданное число кластеров; -- иначе - по высоте `threshold`. - -Это видно непосредственно в -[`fast-clustering.cc`](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/fast-clustering.cc) -и закреплено в комментарии к -[`FastClusteringConfig`](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/fast-clustering-config.h): -при известном числе кластеров порог игнорируется. Python binding публично -принимает C-contiguous матрицу `float32` размера `M x D` и возвращает метку для -каждой строки -([исходник binding](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/python/csrc/fast-clustering.cc)). -Значит, передавать оцененное `N` в существующий кластеризатор не требует новой -реализации распределения эмбеддингов по кластерам. - -Порог `FastClustering` не принадлежит модели эмбеддингов. Это высота -complete-linkage дерева в шкале cosine dissimilarity. Официальный пример -CAMPPlus для speaker verification лишь вычисляет cosine score и не задает -универсальной границы решения -([`infer_sv.py`](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/speakerlab/bin/infer_sv.py)). -Официальный diarization-рецепт 3D-Speaker отдельно задает `pval=0.012` для -spectral clustering и `mer_cos=0.8` для последующего слияния центров -([`infer_diarization.py`](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/speakerlab/bin/infer_diarization.py)). -Эти величины имеют другой смысл и не могут быть перенесены в sherpa как -`порог модели`. - -### Публичных деталей недостаточно для повтора всего конвейера - -Публичный `SpeakerEmbeddingExtractor` позволяет создать stream, подать в него -аудио и получить один embedding -([Python binding](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/python/csrc/speaker-embedding-extractor.cc), -[C/C++ API](https://k2-fsa.github.io/sherpa/onnx/c-api/html/speaker_embedding.html)). -Это пригодно для самостоятельно выбранного фрагмента, но не выдает материал, -который выбрал diarization-конвейер. - -Внутри `OfflineSpeakerDiarization` после сегментации: - -1. из перекрывающихся окон строятся пары `окно x локальный говорящий`; -2. речь с одновременными голосами исключается; -3. для каждой пары собирается свой stream и вычисляется embedding; -4. вся матрица передается в `FastClustering`; -5. метки строк переводятся обратно в разметку говорящих. - -Последовательность и временная матрица видны в -[`OfflineSpeakerDiarizationPyannoteImpl::Process`](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h#L87-L204), -а сборка строк - в -[`GetChunkSpeakerSampleIndexes` и `ComputeEmbeddings`](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h#L376-L528). -Это private-методы реализации. - -Python-класс `OfflineSpeakerDiarization` экспортирует только `sample_rate`, -`set_config` и `process`; `process` возвращает готовый результат, а не признаки -([binding 1.13.5](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/python/csrc/offline-speaker-diarization.cc#L73-L128)). -Внутренний `SetConfig` заменяет только объект кластеризации, тогда как каждый -`Process` заново вызывает segmentation и `ComputeEmbeddings` -([реализация](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h#L79-L175)). -Актуальная публичная C API также документирует только создание, настройку и -получение итоговых сегментов -([официальная страница](https://k2-fsa.github.io/sherpa/onnx/c-api/html/speaker_diarization.html)). - -Итог: публичный API позволяет **кластеризовать имеющуюся матрицу повторно**. -Извлечь или переиграть промежуточную матрицу штатного diarization-конвейера -через него нельзя. - -## Варианты оценки `N` - -### 1. Отдельный порог для каждой embedding-модели - -Это текущий `FastClustering` в автоматическом режиме. Он дешев, не добавляет -зависимостей и на одном проходе сразу оценивает число кластеров и назначает -метки. На порог влияют архитектура модели эмбеддингов, длительность и состав -выбранных фрагментов, акустика и распределение голосов. Локальная калибровка -уже не нашла общего рабочего порога для CAMPPlus, -а TitaNet не прошел автоматическую сетку -([исходное исследование](2026-08-14-diarization-cpu-cost.md), -[повторный CPU-тест](../benchmarks/2026-08-30-lightweight-diarization-cpu.md)). - -Вводить еще один `официальный порог модели` оснований нет: upstream-рецепты -публикуют параметры конкретного clustering pipeline, а не переносимую границу -для complete-linkage sherpa. Вариант остается контрольным, но не решает тикет. - -### 2. Spectral clustering с простым eigengap - -Официальный CAMPPlus-конвейер 3D-Speaker строит cosine affinity, оставляет в -каждой строке фиксированную долю ближайших соседей, симметризует матрицу, -вычисляет ненормированный Laplacian и несколько наименьших собственных пар. -Число говорящих - позиция максимального eigengap в заданных пределах, после -чего собственные векторы кластеризуются k-means -([`SpectralCluster`](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/speakerlab/process/cluster.py#L21-L106)). - -Для нашей двухшаговой схемы нужны только собственные значения и оценка `N`; -k-means можно не выполнять. Это одна spectral decomposition и потому хороший -минимальный контрольный вариант. Однако фиксированный `pval` остается новым -порогом. -Официальная реализация задает его для своего способа нарезки; пары -`окно x локальный говорящий` sherpa устроены иначе. Вариант отделяет `N` от -порога AHC, но сохраняет калибруемый параметр. - -Если брать реализацию 3D-Speaker как зависимость, она импортирует NumPy, SciPy, -scikit-learn, fastcluster, UMAP и HDBSCAN, причем последние два обязательны уже -при импорте модуля -([исходник](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/speakerlab/process/cluster.py#L3-L18), -[requirements](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/requirements.txt)). -Для production это несоразмерно счетчику. - -### 3. NME-SC - -NME-SC создан именно для совместной оценки числа кластеров и параметра -прореживания без dev-set. Для каждого числа соседей `p` алгоритм бинаризует -cosine affinity, симметризует ее, строит ненормированный Laplacian и считает -eigengap. Нормированный gap равен максимальному gap, деленному на наибольшее -собственное значение; выбирается `p`, минимизирующий отношение `p / gap`, а `N` -задает позиция максимального gap -([статья авторов, раздел III](https://arxiv.org/abs/2003.02405)). -Авторы ограничивали поиск `p` диапазоном до четверти числа фрагментов и число -говорящих - восемью; результаты статьи получены на x-vector и англоязычных -корпусах, поэтому перенос на наши эмбеддинги еще предстоит проверить. - -Поддерживаемая реализация NeMo принимает готовые эмбеддинги, строит cosine -affinity и отдельно получает `est_num_of_spk`; известное число говорящих затем -подменяет эту оценку перед spectral clustering -([`offline_clustering.py`](https://github.com/NVIDIA-NeMo/Speech/blob/d47d7a3c306790637aff39c2d3f9eceb83ca8879/nemo/collections/asr/parts/utils/offline_clustering.py#L1087-L1161)). -Для ускорения NME она по умолчанию ограничивает матрицу анализа 512 строками, -перебирает разреженную сетку из 30 значений `p` и предупреждает, что меньше 20 -может ухудшить оценку -([класс `NMESC`](https://github.com/NVIDIA-NeMo/Speech/blob/d47d7a3c306790637aff39c2d3f9eceb83ca8879/nemo/collections/asr/parts/utils/offline_clustering.py#L801-L958)). -Для коротких записей NeMo добавляет случайные anchor embeddings и повторяет -оценку. Это отдельное расширение за пределами минимального ядра статьи -([исходник](https://github.com/NVIDIA-NeMo/Speech/blob/d47d7a3c306790637aff39c2d3f9eceb83ca8879/nemo/collections/asr/parts/utils/offline_clustering.py#L536-L595)). - -Полная NME-SC после оценки `N` еще раз раскладывает Laplacian, строит spectral -embeddings и запускает k-means. Для этого тикета такая замена итогового -кластеризатора не нужна: локальные тесты уже показывают полезный путь -`FastClustering` с известным числом говорящих. Нужна только функция -`embeddings -> N`. - -### 4. Оценить `N`, затем передать известное число кластеров - -Это не отдельный счетчик. Это рекомендуемая граница ответственности: - -```text -segmentation -> embeddings +-> count-only NME -> N - +-> FastClustering(features, num_clusters=N) - -> штатная финализация sherpa -``` - -Плюсы: - -- порог complete-linkage больше не определяет число голосовых кластеров; -- CAMPPlus и TitaNet используют один модельный проход; -- итоговое назначение меток остается в уже принятом FastClustering; -- на одной матрице можно независимо сравнить несколько счетчиков. - -Минус один, но архитектурный: без нового промежуточного интерфейса текущий -`process()` не позволяет вставить счетчик между вычислением матрицы и -кластеризацией. - -## Зависимости, лицензии и цена - -Штатная установка NeMo требует среди прочего NumPy, scikit-learn и PyTorch -([`pyproject.toml`](https://github.com/NVIDIA-NeMo/Speech/blob/d47d7a3c306790637aff39c2d3f9eceb83ca8879/pyproject.toml)). - -| Вариант | Новые runtime-зависимости | Дополнительная работа после эмбеддингов | Ограничение | -|---|---|---|---| -| Порог `FastClustering` | нет | уже существующие попарные расстояния и AHC | порог калибруется для модели и материала | -| Простой eigengap | SciPy в официальной реализации; либо только уже доступный NumPy | affinity `M x M`, одно собственное разложение | фиксированный параметр прореживания | -| Полная NME-SC NeMo | `nemo-toolkit`, PyTorch, scikit-learn и другие зависимости NeMo | до нескольких десятков eigenvalue-разложений, затем еще spectral embedding и k-means | самый тяжелый и меняет итоговый кластеризатор | -| Счетчик NME + FastClustering | только NumPy при чистой реализации ядра статьи | NME на ограниченной матрице, затем штатный AHC по известному `N` | нужен промежуточный интерфейс и локальная проверка переноса | - -`FastClustering` уже хранит `M(M-1)/2` расстояний и строит AHC. Простой -eigengap добавляет еще одну плотную affinity-матрицу. NME повторяет eigenvalue- -анализ для набора `p`; ограничение размера и разреженный поиск контролируют -цену, но их влияние на качество нужно измерить. Полная spectral clustering -также хранит собственные векторы и запускает k-means, поэтому для одной оценки -`N` избыточна. В проектных замерах основной расход по-прежнему приходится на -извлечение эмбеддингов, а не на clustering -([профиль стоимости](2026-08-14-diarization-cpu-cost.md), -[CPU-тест](../benchmarks/2026-08-30-lightweight-diarization-cpu.md)); это не -заменяет отдельного замера NME на целевой матрице. - -sherpa-onnx, 3D-Speaker и NeMo Speech распространяют код под Apache-2.0 -([sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/LICENSE), -[3D-Speaker](https://github.com/modelscope/3D-Speaker/blob/065629c313eaf1a01c65c640c46d77e61e9607b4/LICENSE), -[NeMo Speech](https://github.com/NVIDIA-NeMo/Speech/blob/d47d7a3c306790637aff39c2d3f9eceb83ca8879/LICENSE)). -Авторская реализация NME-SC опубликована под MIT и сама направляет пользователей -к поддерживаемой версии NeMo -([репозиторий авторов](https://github.com/tango4j/Auto-Tuning-Spectral-Clustering/tree/ae97f9be9c33b554205c7192c5137ce8f456c1b7)). -Файл NeMo с clustering дополнительно сохраняет BSD-уведомление scikit-learn. -NumPy имеет BSD-3-Clause -([лицензия 2.4.3](https://github.com/numpy/numpy/blob/v2.4.3/LICENSE.txt)) и уже -присутствует в [lock-файле проекта](../../uv.lock). Лицензионного запрета на -прототип нет; -прямое заимствование кода потребует сохранить его уведомления. Новые модели -для счетчика не нужны. - -## Ограничения вывода - -- NME-SC проверялась авторами на другой нарезке и x-vector. Строки sherpa - это - локальные говорящие перекрывающихся Pyannote-окон после исключения overlap; - переносимость нельзя считать доказанной. -- Правильное `N` не гарантирует качественную разметку говорящих. Для TitaNet в - режиме известного числа кластеров уже наблюдался малый кластер, поэтому нужны - прежние proxy-метрики и слуховая проверка. -- У sherpa есть отдельный путь для единственного окна, который обходится без - общей матрицы эмбеддингов. Прототип не должен ломать этот special case. -- Python binding `FastClustering` нормирует переданную матрицу на месте; - сравниваемым вариантам нужно выдавать отдельные C-contiguous копии. -- Цена NME на числе строк, которое реально создает sherpa, здесь не измерялась. - -## Точная граница будущего прототипа - -Production-код, CLI, модели по умолчанию и ADR не менять. Сделать отдельный -исследовательский стенд на закрепленном sherpa-onnx 1.13.5: - -1. Узко разделить внутренний `Process` на подготовку и финализацию либо добавить - исследовательский callback после `ComputeEmbeddings`. За один проход - сохранить в памяти матрицу `float32`, список валидных пар - `окно x локальный говорящий` и локальные frame labels; аудио и текст не - сериализовать. -2. На **одной и той же** матрице сравнить три счетчика: - текущий порог как контроль, простой eigengap с фиксированным прореживанием и - count-only NME. Ограничить `N` диапазоном `1..8`. -3. Минимальный NME реализовать по статье на NumPy: cosine affinity, - top-`p` binarization, симметризация, ненормированный Laplacian, - `numpy.linalg.eigvalsh`, выбор `p` и `N` по NME. Для первого прогона взять - детерминированное ограничение 512 строк и sparse search из 20 значений; - anchor embeddings, Torch, spectral embeddings и k-means не включать. -4. Для каждого оцененного `N` вызвать публичный `FastClustering` на копии той же - матрицы с `num_clusters=N`, затем выполнить существующую финализацию без - повторной сегментации или извлечения эмбеддингов. -5. На тех же трех контрольных фрагментах измерить: оцененное `N`, общее и - содержательное число кластеров, mapped speaker purity, слова без говорящего, - время счетчика/кластеризации и peak RSS. Прогон с CAMPPlus и TitaNet должен - завершиться слуховой проверкой. -6. Если простой eigengap и NME одинаково проходят проверку, выбрать более - дешевый eigengap. Если проходит только NME, отдельно решить, переносить ли - чистую NumPy-реализацию в production. Если оба ошибаются, быстрые embedding- - модели оставить только для явно заданного числа говорящих. - -Этот прототип отвечает ровно на два неизвестных: можно ли стабильно оценить -`N` по внутренним эмбеддингам CAMPPlus/TitaNet и укладывается ли count-only NME -в приемлемую CPU-цену. Он не выбирает новую модель по умолчанию, не заменяет -FastClustering и не расширяет продуктовый контракт. -- 2.54.0 From 6f0adf2eb04861b38464c550ff3d02fe02d05675 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sun, 30 Aug 2026 21:05:40 +0300 Subject: [PATCH 6/6] =?UTF-8?q?docs(cpu):=20=D0=B2=D1=8B=D1=87=D0=B8=D1=89?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=84=D0=BE=D1=80=D0=BC=D1=83=D0=BB=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=B0=D0=BF=D0=BF=D0=B0=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D0=BD=D1=8B=D1=85=20baseline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - README и backlog должны одинаково и однозначно описывать границы CPU-замеров перед merge. - Что: - Замер Ryzen ограничен одной зафиксированной тестовой машиной. - Из backlog убраны повтор и искусственные противопоставления вокруг целевого SKU. - Проверка: - `git diff --check`. --- README.md | 4 ++-- docs/backlog.md | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 88a75d4..5812799 100644 --- a/README.md +++ b/README.md @@ -353,8 +353,8 @@ faster-whisper её не поддерживает, и запуск с `--device | `parakeet-v3` | ~600 MB | 7,6×* | 25 языков | ✅ | \* Наблюдение на AMD Ryzen 7 8845H, Windows, `int8`, три записи общей -длительностью 43:37. Это результат на одной из доступных тестовых машин, а не -прогноз производительности на других CPU. +длительностью 43:37. Замер описывает только эту тестовую машину; скорость на +других CPU требует отдельного прогона. Методика и качественное сравнение: [benchmark GigaAM и Whisper](docs/benchmarks/2026-08-11-gigaam-model-comparison.md). diff --git a/docs/backlog.md b/docs/backlog.md index 6f75754..b58e93b 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -13,7 +13,7 @@ **Промежуточный статус 2026-08-11:** короткая матрица на трёх записях завершена и описана в [benchmark GigaAM и Whisper](benchmarks/2026-08-11-gigaam-model-comparison.md). Multilingual Large добавлена как явный качественный профиль; решение о default -по-прежнему ждёт длинных негативных примеров и проверки производительности на +по-прежнему ждет длинных негативных примеров и проверки производительности на нескольких доступных классах CPU. Под CPU-дефолтом в этом разделе понимается программный профиль: backend, модель @@ -76,12 +76,13 @@ Multilingual Large добавлена как явный качественный - использовать минимум два доступных x86-ориентира: консервативный baseline на Intel Core i7-6820HQ и современный baseline на AMD Ryzen 7 8845H; - для каждого прогона записывать точный SKU, физические ядра и логические - потоки, CPUID/ISA, объём RAM, power mode и фактическое число потоков; + потоки, CPUID/ISA, объем RAM, power mode и фактическое число потоков; - Ryzen использовать как основную машину для итераций и как современный - контролируемый baseline, а не переносить его результат на остальные CPU; + контролируемый baseline. Результат относится только к зафиксированной + конфигурации; - при доступности отдельно собирать результаты на других x86-ноутбуках и Apple - Silicon как данные о переносимости. Такие внешние прогоны не заменяют - контролируемую матрицу; + Silicon как данные о переносимости; в контролируемую матрицу входят два + предыдущих x86 baseline; - измерять отдельно cold start/загрузку модели и warm transcription, медиану трёх прогонов, RTFx, peak RSS и размер скачиваемой модели; - короткий 15-минутный фрагмент нужен для итераций, полные записи 22–81 мин — @@ -148,10 +149,9 @@ Multilingual Large добавлена как явный качественный **Приоритет:** высокий - проблема затрагивает текущий OpenVINO CPU-путь, а пользователи работают на ноутбуках разных поколений и производителей. -Встречаются и машины уровня Intel Core i5 11-го поколения, но это пример -распространенной конфигурации, а не целевой SKU. GigaAM v3 остается рабочей -явной альтернативой для русского, но выбор безопасного auto/default требует -сравнительного бенчмарка. +Среди них встречаются и более скромные машины уровня Intel Core i5 11-го +поколения. GigaAM v3 остается рабочей явной альтернативой для русского, но +выбор безопасного auto/default требует сравнительного бенчмарка. --- -- 2.54.0