docs(diarization): спроектирован следующий CPU-эксперимент

- Зачем:
  - нужен ограниченный эксперимент для отсева явных проигрышей без полного перебора и лишней точности.
- Что:
  - зафиксирована поэтапная матрица проверок на Intel и Ryzen.
  - определены пороги скорости, условия качества, контроль памяти и приватности.
  - описаны воспроизводимость стенда, QDQ-происхождение данных и правила решения.
- Проверка:
  - `.venv/bin/pytest -q tests/test_diarization_calibration.py tests/test_speaker_diarizer.py tests/test_diarization.py` (61 passed).
This commit is contained in:
Dmitry Dementiev committed 2026-08-31 00:16:09 +03:00
1 parent e7c6eacb56
commit fde4c48859
1 file changed
+474
@@ -0,0 +1,474 @@
# Следующий CPU-эксперимент диаризации
**Дата:** 2026-08-30
**Статус:** согласованная постановка эксперимента. Production-конфигурация не
менялась.
## Проблема
Текущий профиль Pyannote FP32 + WeSpeaker тратит большую часть времени
диаризации на эмбеддинги для сильно перекрывающихся окон. Отдельные исследования
нашли три независимых рычага: увеличить шаг окна, удешевить эмбеддинги и изменить
их исполнение. Полный перебор всех сочетаний слишком дорог и смешивает проверку
механизма с выбором итогового профиля.
Нужен один эксперимент с последовательным отсевом. Он должен показать, есть ли
комбинация, которая сокращает время диаризации на Ryzen минимум на 30%, сохраняет
принятую проверку качества и остается работоспособной на старом Intel. Ориентир
для сильного результата - ускорение примерно в два раза.
## Сначала прочитать
- [ADR-007](../adr/007-word-level-speaker-diarization.md) - когда работа касается
действующего профиля и проверки качества.
- [Предыдущий CPU-тест](../benchmarks/2026-08-30-lightweight-diarization-cpu.md) -
при работе с корпусом, метриками и приватными результатами.
- [Шаг окна](../research/2026-08-30-sherpa-window-shift.md) - при добавлении
`window_shift_ratio` и счетчиков работы.
- [Оценка числа говорящих](../research/2026-08-30-diarization-speaker-count.md) -
при реализации `Prepare/Finalize`, eigengap и NME.
- [Embedding-инференс](../research/2026-08-30-diarization-embedding-inference.md) -
при квантизации, конкурентных stream и batch TitaNet.
- [Решение об аппаратной матрице](https://git.dementev.space/ddmitry/local-transcriber/issues/35#issuecomment-2243)
- при подготовке машин и разборе памяти.
## Цель и границы
Эксперимент должен:
- расширить исследовательский стенд для новых ручек, повторов и счетчиков;
- независимо проверить сокращение работы, модель с надежной оценкой числа
говорящих и способы ускорить embedding-инференс;
- собрать не больше трех комбинаций на Intel и перенести не больше двух на
Ryzen;
- выбрать один итоговый вариант либо зафиксировать, почему ни один вариант не
прошел;
- выпустить публичный обезличенный отчет и сохранить полную переносимую капсулу
в закрытой папке Яндекс Диска.
В эксперимент не входят:
- изменение CLI, `.transcriber.toml` или модели по умолчанию;
- автоматический профиль по ISA или отдельные пользовательские профили;
- параллельный запуск ASR и диаризации, GPU, NPU и Mac;
- новая полная спектральная кластеризация, NeMo runtime и k-means;
- CAMPPlus batch без маски и несколько сессий как самостоятельное направление;
- production-переход на победителя по результату одного эксперимента.
Если каскад не достигает нужной скорости из-за качества, следующий шаг -
отдельная карта по end-to-end диаризации. Этот эксперимент ее не начинает.
## Совпадающий baseline
Каждое сравнение использует baseline, запущенный на той же машине, в том же
режиме питания и на том же стенде:
- `sherpa-onnx` 1.13.6;
- Pyannote Segmentation 3.0 FP32;
- WeSpeaker ResNet34 LM FP32;
- `window_shift_ratio=0.1`;
- `FastClustering` с порогом `0.89` и автоматическим числом кластеров;
- последовательный embedding-инференс;
- не больше `P` потоков, где `P` - число физических ядер машины.
Версию `sherpa-onnx`, uv override и `sherpa-onnx-core` обновляют синхронно.
Значение `0.1` сохраняет действующее поведение. Изменение зависимости само по
себе не считается ускорением. На тех же трех фрагментах W0 сравнивается со
свежим запуском 1.13.5 на той же машине. После канонической нумерации кластеров
сверяются интервалы, назначение говорящих словам и прежние диагностические
метрики. Различия направляются в слуховую проверку отмеченных мест. Baseline
1.13.6 принимается только при `3 / 2 / 2` и отсутствии подтвержденной регрессии.
Время диаризации во всей спецификации означает wall time одного вызова
`diarizer.process(samples)`. ASR, загрузка моделей, декодирование и подготовка
данных в него не входят.
Три порога используются во всех этапах:
- разница до 5% считается равенством по скорости;
- 10% - порог продолжения после дешевого отсева;
- 30% - итоговый порог `go` на Ryzen.
## Стенд и данные
Существующий `scripts/benchmarks/diarization_calibration.py` остается точкой
входа, но получает новую версию manifest. Прежний формат жестко закрепляет
матрицу FP32/INT8 x WeSpeaker/TitaNet, обязательный свип порогов и
`sherpa-onnx` 1.13.5, поэтому старые результаты с новым экспериментом не
продолжаются.
Новая версия manifest и идентификатор ячейки должны учитывать:
- вариант шага окна, модель, счетчик `N` и способ embedding-инференса;
- число outer workers, вид session и бюджеты intra-op/inter-op;
- размер batch, где он применим;
- машину, число физических ядер, CPUID/ISA и версии библиотек;
- номер повтора и позицию в расписании;
- хеш экспериментальной сборки или патча sherpa-onnx;
- хеши моделей и приватных входов по прежнему контракту.
Для QDQ идентичность также включает хеши калибровочных источников,
препроцессинг, reader, параметры `quant_pre_process` и `quantize_static`, список
исключенных узлов, версии инструментов и хеш готового графа. Для каждого
калибровочного элемента manifest хранит идентификатор и SHA-256 исходной записи,
а также использованный временной интервал. Вырезанный или перекодированный файл
наследует эти данные от исходника. Ни один калибровочный элемент не может
происходить из трех контрольных записей, даже из другого интервала.
`--validate-only` отклоняет общий исходник; если происхождение нельзя подтвердить
или отдельного материала нет, ветвь QDQ пропускается и не блокирует остальные
направления. Проверка показывает только хеши и рецепт без приватных путей.
Каждая измеряемая ячейка выполняется в новом дочернем процессе. Стенд сохраняет
сырые результаты каждого повтора и не заменяет их одной агрегированной строкой.
При продолжении запуска успешные ячейки пропускаются по стабильному
идентификатору, а расписание оставшихся сохраняет заданный A/B-порядок. Режим
`--validate-only` показывает полный план без приватных путей и текста.
### Приватность
За пределами Git остаются manifest, записи, WAV-кеш, ASR-sidecar, опорные
транскрипты, сырые интервалы, результаты, worker requests, журналы и материалы
слуховой проверки. Рабочий каталог стенда нельзя размещать в обычной
`.scratch`: репозиторий игнорирует там только `.scratch/handoffs/`.
Публичный отчет содержит хеши, агрегаты, конфигурацию и выводы. В него не входят
локальные пути, распознанный текст, временные интервалы, имена участников и
фрагменты приватных записей. Перед внешним ревью используется только публичный
отчет и код стенда.
## Воронка вариантов
### Этап 0. Проверить механизмы без дорогих замеров
До CPU-матрицы стенд должен пройти дешевые проверки:
1. `sherpa-onnx` 1.13.6 импортируется, принимает пять значений шага и сохраняет
прежнее поведение при `0.1`.
2. Счетчик eigengap и NME проходят синтетические фикстуры из исследования.
Затем оба считаются на независимых копиях трех матриц CAMPPlus и трех матриц
TitaNet, подготовленных без повторного инференса. Для каждой модели отдельно
остается счетчик, который дает точные `3 / 2 / 2`. Если проходят оба, при
разнице времени до 10% выбирается более простой eigengap, иначе более
быстрый. Модель, для которой не проходит ни один счетчик, выбывает из
автоматического режима.
3. `Prepare/Finalize` сохраняет пары строк и штатную финализацию. Для известного
`N` метки строк и итоговая разметка совпадают с отдельным штатным вызовом на
той же матрице.
4. WeSpeaker S8S8 QDQ сохраняет входы, выходы, opset, пользовательские метаданные
и загружается через `SpeakerEmbeddingExtractor`.
5. Shared-session возвращает по одному результату на каждый вход без гонок,
пропусков и нечисловых значений. Сравнение ведется с последовательными
вызовами на том же наборе входов.
Числовой дрейф эмбеддингов записывается, но отдельный произвольный порог для него
не вводится. Решение принимается по одинаковому числу кластеров, меткам строк и
итоговой разметке.
### Этап 1. Найти безопасный шаг окна на Intel
На Core i7-6820HQ проверяется текущая связка WeSpeaker с сеткой:
| Ячейка | `window_shift_ratio` | Роль |
|---|---:|---|
| W0 | `0.10` | совпадающий baseline |
| W1 | `0.15` | осторожное сокращение окон |
| W2 | `0.20` | примерно половина исходных окон |
| W3 | `0.25` | агрессивный кандидат |
| W4 | `0.50` | граничная проверка |
Для каждой ячейки выполняется один прогон трех пятиминутных фрагментов. Стенд
записывает фактический шаг в сэмплах, расчетное число segmentation-окон и число
embedding-заданий. Ускорение без соответствующего изменения счетчиков считается
необъясненным и не проходит дальше до разбора.
Все значения, прошедшие обязательные условия, ранжируются по времени
диаризации. При равенстве по скорости выбирается меньший шаг: он меньше отходит
от принятой разметки. Если лучший по времени шаг ухудшает purity или число слов
без говорящего, до сборки комбинаций прослушиваются отмеченные пятиминутные
случаи. Рабочим становится лучший шаг, достигший порога продолжения. Если такого
нет, рабочим остается `0.1`, а ветвь шага считается проигравшей.
### Этап 2. Выбрать модель и счетчик говорящих на Intel
При `window_shift_ratio=0.1` сравниваются:
| Ячейка | Эмбеддинги | Оценка числа говорящих |
|---|---|---|
| M0 | WeSpeaker FP32 | текущий `FastClustering`, порог `0.89` |
| M2 | CAMPPlus | счетчик, выбранный для CAMPPlus, + `FastClustering(N)` |
| M3 | TitaNet small | счетчик, выбранный для TitaNet, + `FastClustering(N)` |
M0, M2 и M3 получают по одному прогону трех фрагментов. Быстрая модель проходит
дальше, если выдерживает обязательные условия и достигает порога продолжения
относительно M0. Малая разница не уточняется дополнительными повторами.
Регрессия диагностических метрик требует прослушать отмеченные пятиминутные
случаи до сборки комбинации.
### Этап 3. Проверить исполнение эмбеддингов
#### QDQ WeSpeaker
S8S8 QDQ сравнивается с FP32 на обеих машинах при шаге `0.1`. На Intel
достаточно одного A/B по трем фрагментам. Изолированный Ryzen A/B выполняется
до сборки комбинаций этапа 4: по одному прогону baseline и кандидата на трех
фрагментах. Проверка на Ryzen обязательна даже при проигрыше на Intel: выигрыш
зависит от VNNI/AVX-512.
U8U8 допускается как единственный запасной вариант, если S8S8 достигает порога
продолжения по времени диаризации, но не проходит качество. Другие форматы
квантизации в матрицу не добавляются.
#### Shared-session streams
На Intel и Ryzen сравниваются четыре конфигурации при общем бюджете `P`. На
Intel достаточно одного A/B по трем фрагментам. Изолированный Ryzen A/B также
выполняется до этапа 4 и состоит из одного прогона baseline и одного прогона
кандидата на трех фрагментах. Порядок baseline и кандидата меняется между
парами. Эти прогоны в
этапе 5 не повторяются.
| Ячейка | Outer workers | Intra-op | Session |
|---|---:|---:|---|
| E0 | `1` | `P` | одна, текущий внутренний параллелизм |
| E1 | `2` | общий pool `P` | одна |
| E2 | `P` | `1` | одна |
| E3 | `P` | `1` на session | отдельные session, отрицательный контроль |
`inter_op_threads=1` во всех ячейках. Измеряется время полного набора
embedding-вызовов. Latency одного удачного stream для решения не используется.
E3 не становится кандидатом, если только не опровергает ожидаемый рост RSS и не
выигрывает у shared-session по времени диаризации.
#### Batch TitaNet
Batch TitaNet открывается только после прохождения M3. Сначала сравниваются batch
`1` и `4` на одинаковом наборе переменных длин. Batch проходит к CPU-замеру,
если сохраняет число кластеров, метки строк и итоговую разметку на всех трех
контрольных матрицах. CAMPPlus batch и подбор размера больше `4` отложены.
### Этап 4. Собрать комбинации на Intel
Полного декартова перебора нет. На Intel собираются максимум три явно заданные
комбинации:
| Ячейка | Состав |
|---|---|
| C0 | рабочий шаг + WeSpeaker FP32 + последовательный инференс |
| C1 | рабочий шаг + одна сильнейшая прошедшая ветвь |
| C2 | рабочий шаг + все совместимые прошедшие ветви |
Сильнейшая ветвь определяется по сокращению времени диаризации: для быстрой
модели используется Intel, для QDQ и shared-session - обязательный изолированный
Ryzen A/B из этапа 3. Это намеренно грубое межмашинное ранжирование: Intel
дешево отсеивает быстрые модели, а ISA-чувствительные ветви оцениваются там, где
их выигрыш проявляется. Оно не заменяет финальный Ryzen A/B. QDQ совместим
только с WeSpeaker. Batch совместим только с TitaNet. Shared-session добавляется
к выбранной модели после проверки сохранности всех входов и результатов на этой
модели. Если C1 и C2 совпадают, дубликат не запускается. C0 совпадает с baseline
и тоже не запускается, если рабочим остался шаг `0.1`.
Каждая существующая комбинация получает один прогон трех фрагментов. Если C0
отличается от baseline, он всегда переносится на Ryzen после обязательных
условий, а из C1 и C2 туда идет максимум один вариант с лучшим временем
диаризации. Если C0 совпадает с baseline, на Ryzen переносятся максимум два
разных варианта C1/C2. При равенстве по скорости выбирается меньший RSS и
меньшее изменение sherpa-onnx. Если прошел только новый шаг окна, воронка
продолжается с одним C0. Если порога продолжения не достигла ни одна ветвь,
эксперимент фиксирует `stop` до финального Ryzen A/B.
### Этап 5. Принять финалиста на Ryzen
На Ryzen выполняются:
- совпадающий baseline;
- не больше двух комбинаций из этапа 4.
Изолированные QDQ и shared-session A/B уже выполнены в этапе 3 и здесь не
повторяются.
Первичный A/B состоит из одного прогона baseline и одного прогона кандидата на
трех фрагментах. Порядок меняется для следующей пары. Вариант, который не
достигает порога продолжения, выбывает без дополнительных прогонов.
Не больше двух вариантов, прошедших первичный A/B, получают по пять чередующихся
A/B-циклов: C0, если он отличается от baseline, и лучший по времени диаризации
из C1/C2; иначе два лучших варианта C1/C2. Один цикл содержит все три фрагмента;
результат цикла - сумма времени диаризации. Итогом служат медиана пяти парных
ускорений и MAD. Доверительные интервалы и новые повторы после пяти циклов не
требуются.
Итоговая комбинация проходит условие скорости, если достигает итогового порога
на Ryzen. Примерно двукратное ускорение остается ориентиром. На Intel процент
допустимого замедления заранее не задается. Если финалист медленнее Intel
baseline минимум на порог продолжения, эксперимент возвращает `needs-decision`
вместо автоматического `go`: приемлемость фактической регрессии решает владелец
после замеров.
### Этап 6. Проверить полные записи
Один победитель и совпадающий baseline 1.13.6 однократно обрабатывают три полные
записи на Ryzen. Эти прогоны проверяют перенос результата за границы
пятиминутных фрагментов; единственное время полного прогона не заменяет
итоговый порог скорости.
В слуховой проверке baseline и кандидат сопоставляются по пяти группам:
- короткие ответы и перебивки;
- смены говорящего;
- перекрывающуюся речь;
- устойчивость соответствия голоса кластеру;
- слова без говорящего и малые остаточные кластеры.
Вердикт и краткие причины сохраняются в приватной капсуле. Публичный отчет
содержит только итог `пройдено` или `не пройдено` по каждой категории.
## Метрики
### Работа и скорость
Для каждой ячейки сохраняются:
- wall time, user/system/total CPU, средняя загрузка ядер и RTF;
- времена segmentation, embeddings, clustering и всего вызова;
- фактический шаг, число segmentation-окон и embedding-заданий;
- число строк подготовленной матрицы, выбранные `p` и `N`, время счетчика;
- число embedding-вызовов, batch, workers, session и бюджеты потоков.
### Качество
Обязательные условия:
- точное число кластеров `3 / 2 / 2` на контрольных фрагментах;
- неизменный распознанный текст с точностью до прежней нормализации пробелов;
- повторяемые `p`, `N`, метки строк и финализация на одной матрице;
- конечные эмбеддинги и отсутствие потерь или дублей входов.
Диагностика:
- `mapped speaker purity` на двух записях с опорной разметкой;
- число слов без говорящего;
- число и длительность малых кластеров;
- доля времени речи в кластерах сверх ожидаемых;
- доля назначенных слов в тех же кластерах.
Последние два показателя получают разные имена и единицы. Дополнительными
считаются кластеры после `N` крупнейших по времени речи. Доля времени использует
общее время речи в знаменателе, доля слов - все слова, назначенные известным
кластерам.
Прокси-метрики не получают произвольных жестких порогов. Ухудшение purity или
рост слов без говорящего относительно совпадающего baseline помечает случаи для
слуховой проверки. Победитель не принимается, если слуховая проверка
подтверждает регрессию коротких реплик, смен говорящего или устойчивости
кластеров.
ASR выполняется один раз при подготовке sidecar и не входит в измеряемые ячейки.
Каждая ячейка использует тот же набор слов. Проверка неизменности текста
сравнивает сборку слов после назначения говорящих с sidecar и ловит потерю,
дубль или перестановку слов; она не запускает ASR повторно.
### Память и машина
Для каждого запуска фиксируются точный SKU, физические и логические ядра, RAM,
CPUID/ISA, ОС, версии библиотек, питание, power mode, governor, turbo, power
limits, температуры, throttling, swap и число потоков.
Обе машины работают от сети в стабильном `balanced`-режиме. A/B чередуются,
чтобы прогрев не давал преимущества одному варианту. Прогон с throttling
или сменой режима питания не учитывается и повторяется один раз после
стабилизации. Абсолютный температурный порог не задается.
Peak RSS записывается для каждой ячейки и сравнивается с baseline той же машины.
RSS выше 2 ГиБ на машине с 16 ГиБ RAM требует разбора, но сам по себе не
отбрасывает вариант. Эксперимент останавливает ячейку при OOM, активной
подкачке, росте RSS между повторами без плато или линейном росте памяти с числом
workers или session.
## Правила решения
Результаты рассматриваются в таком порядке:
1. обязательные условия качества и корректности;
2. медианное сокращение времени диаризации на Ryzen;
3. память;
4. сложность будущей реализации и сопровождения.
Практически незначимая разница не получает дополнительных прогонов. При
равенстве по скорости выбирается более простой и экономный по памяти вариант.
Ни один экспериментальный результат автоматически не меняет production-профиль.
Возможны четыре исхода:
- `go`: один вариант достигает итогового порога на Ryzen и проходит все
проверки;
- `needs-decision`: Ryzen проходит итоговый порог, но Intel показывает
практически значимую регрессию;
- `stop`: ни один вариант не проходит скорость, качество или память; причины
сохраняются по каждой ветви;
- `new map`: сокращение окон и цены эмбеддинга упирается в качество раньше
порога скорости, поэтому отдельно рассматривается end-to-end архитектура.
## Проверка реализации стенда
Минимальная автоматическая проверка:
```bash
uv run pytest -q \
tests/test_diarization_calibration.py \
tests/test_speaker_diarizer.py \
tests/test_diarization.py
```
Тесты должны покрывать новую схему manifest и идентичность ячеек, расписание и
повторы, смешанные результаты счетчиков, ветвь только с шагом окна, совместимый
стек, наследование происхождения QDQ-материала и запрет общего исходника с
контрольными записями, обязательные условия, два показателя остаточных кластеров,
контроль общего бюджета потоков, продолжение незавершенного запуска и отсутствие
приватного текста в ошибках.
До моделей выполняются:
```bash
uv run python scripts/benchmarks/diarization_calibration.py --help
uv run python scripts/benchmarks/diarization_calibration.py \
--manifest /private/path/manifest.json \
--validate-only
```
После эксперимента публичный отчет должен позволять восстановить все решения по
агрегатам и хешам. Полное воспроизведение использует приватную капсулу из
закрытой папки Яндекс Диска.
## Отклоненные варианты
- Полный декартов перебор отклонен: большая часть ячеек повторяет уже
проигравшие механизмы и не влияет на выбор.
- Пять повторов каждой ячейки отклонены: дорогая точность нужна только для
финального A/B. Первичный отсев использует порог продолжения.
- Только шаг окна отклонен: он не проверяет более дешевые модели и исполнение,
которые могут складываться с сокращением работы.
- Только современный CPU отклонен: он скрывает регрессии на AVX2 и не дает
понять, зависит ли выигрыш от ISA.
- Полная матрица на Ryzen отклонена: старый Intel дешевле для первичного отсева,
а Ryzen нужен для ISA-чувствительных A/B и финальной комбинации.
- Сохранение текущего профиля остается допустимым итогом, если практического
победителя нет.
## Критерии завершения задачи эксперимента
- [ ] Стенд и тесты реализуют описанную воронку и не меняют пользовательский
контракт диаризации.
- [ ] Совпадающий baseline воспроизведен на Intel и Ryzen.
- [ ] Все пройденные и остановленные ветви имеют явную причину и ссылку на
ячейки результата.
- [ ] Финальный A/B завершен либо зафиксировано, почему до него не дошел ни один
вариант.
- [ ] При наличии победителя выполнена проверка полных записей и слуховая
приемка.
- [ ] Публичный отчет свободен от приватных данных; полная капсула сохранена
только в закрытой папке Яндекс Диска.
- [ ] Итог формулирует следующее решение: готовить production-изменение,
вынести измеренную регрессию на Intel на решение владельца, сохранить текущий
профиль или открыть новую карту end-to-end диаризации.