Files
local-transcriber/docs/benchmarks/2026-08-31-cpu-diarization-ryzen.md
T
Dmitry Dementiev 89475b9b97 docs(diarization): спроектирована end-to-end карта
- Зачем:
  - локальные оптимизации каскада не достигли 30%-го порога ускорения;
  - старые закрытые хранилища и капсулы не должны стать будущей зависимостью.
- Что:
  - собран единый реестр результатов задач #41 и #42;
  - опубликовано разложение Ryzen baseline и выбрана архитектурная гипотеза;
  - подготовлены самодостаточная спецификация и ready-for-agent задача #46.
- Проверка:
  - пройдены self-review и две независимые линии ревью;
  - проверены 44 строки реестра, 42 арифметических инварианта и ссылки;
  - выполнены ai-text-lint, privacy scan и post-lint guard.

Refs: #45
2026-09-01 16:26:58 +03:00

327 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CPU-диаризация: Ryzen-этап эксперимента
**Дата:** 2026-08-31
**Статус:** `new map`; C2 прошла проверки качества и памяти, но не достигла
итогового порога ускорения 30%, а более быстрые ветви Intel уперлись в качество.
Production-профиль не менялся.
## Итог
На AMD Ryzen 7 8845H выполнен этап 5 эксперимента из
[спецификации](../specs/2026-08-30-cpu-diarization-experiment.md) по единственному
кандидату C2 из [Intel-этапа](2026-08-31-cpu-diarization-intel.md). Этап 6 не
запускали: победителя нет. C2 сохраняет `window_shift_ratio=0.1`, Pyannote
Segmentation 3.0 FP32, WeSpeaker ResNet34 LM FP32 и `FastClustering(0.89)`.
Embedding-вызовы выполняют два outer worker через одну общую ONNX Runtime
session с общим пулом из восьми intra-op потоков; `inter_op_threads=1`.
Изолированный A/B механизма shared-session дал ускорение 24,05%. Первичный A/B
C2 показал 22,90%, поэтому кандидат прошел порог продолжения 10%, и для него
запустили пять чередующихся циклов. Они дали медиану 23,57% при MAD 0,62
процентного пункта, то есть ниже итогового порога 30%; по одному Ryzen-кандидату
C2 итог был бы `stop`.
Вся воронка дала `new map`. W1-W4 потеряли обязательное число кластеров. C1 с
CAMPPlus + NME ускорила Intel-прогон на 33,65%, но оставила два дополнительных
слова без говорящего; требуемую слуховую проверку не проводили и автоматической
оценкой не подменяли. Поэтому C1 не передавали на Ryzen и не считаем победителем.
Вернуть ее в матрицу можно только после приватной слуховой проверки.
Все принятые ячейки прошли обязательное качество: `3 / 2 / 2` кластера,
неизменный текст, `51 / 13 / 9` слов без говорящего, те же значения
`mapped speaker purity` `0,76655 / 0,78742`, `291 / 291 / 291` segmentation-окон
и `531 / 414 / 440` embedding-заданий. Потерь или дублей prepared-строк не было.
Пиковый RSS C2 оставался в диапазоне 410,3-418,1 МиБ; OOM, активности swap и
линейного роста памяти не наблюдалось.
Полные записи и слуховая приемка по пяти категориям на этапе 6 не запускались.
## Что именно проверяли
Отчет охватывает всю воронку: этапы 0-4 на Intel и этап 5 на Ryzen. На Intel
проверили шаг окна, модели эмбеддингов, счетчик говорящих, квантизацию, способы
параллельного исполнения и две комбинации. Таблицы ниже собирают этот путь в
одном месте. Подробные хеши и исходные идентификаторы ячеек остаются в
[Intel-отчете](2026-08-31-cpu-diarization-intel.md).
Intel-ячейки W/M/E/C были разведочными одиночными прогонами трех пятиминутных
фрагментов. Они показывают направление и причину отсева, но не дисперсию.
Исключение - соседняя пара W0/C2 под runtime guard. На Ryzen сначала выполнялась
одна диагностическая пара, затем первичная пара C2 и только после прохождения
порога - пять чередующихся циклов.
### Этап 0: пригодность механизмов
До дорогих CPU-ячеек проверили, что экспериментальный стенд вообще измеряет
задуманные механизмы, не теряя строки и не меняя пользовательский результат.
| Гипотеза | Что проверяли | Что получилось | Решение |
|---|---|---|---|
| `Prepare/Finalize` | Подготовка один раз, кластеризация копии матрицы и финализация | Результат совпал со штатным `process()`; пропусков и дублей строк нет | Использовать в стенде |
| Счетчик eigengap | CAMPPlus и TitaNet на матрицах `531 / 414 / 440` строк | CAMPPlus дал `6 / 1 / 7`, TitaNet - `7 / 2 / 4` вместо `3 / 2 / 2` | Исключить |
| Счетчик NME | Те же шесть матриц без повторного embedding-инференса | Обе модели дали `3 / 2 / 2`; метки повторяемы, потерянных строк нет | Использовать для M2 и M3 |
| WeSpeaker S8S8 QDQ | Схема, opset, метаданные, динамические длины и загрузка через sherpa-onnx | Граф технически корректен; качество и скорость проверены отдельно в Q1 | Допустить к CPU-ячейке |
| Shared-session worker | Один и два worker на одном наборе prepared-входов | Строки, метки и интервалы совпали; гонок и нечисловых значений нет | Допустить E1/E2 |
Задача этапа 0 - подтвердить, что последующие CPU-ячейки сравнивают одинаковые
вычисления и используют исправный экспериментальный API. Скорость измеряли на
следующих этапах.
### Этап 1: можно ли считать меньше окон
Гипотеза: увеличить `window_shift_ratio`, сократить число segmentation-окон и
вместе с ними число embedding-заданий. Все варианты использовали Pyannote
Segmentation 3.0 FP32, WeSpeaker FP32 и текущий `FastClustering(0.89)`.
| Ячейка | Шаг | Окна по трем фрагментам | Embedding-задания | Время, с | Кластеры | Решение |
|---|---:|---|---|---:|---|---|
| W0 | 0,10 | `291 / 291 / 291` | `531 / 414 / 440` | 148,220 | `3 / 2 / 2` | baseline |
| W1 | 0,15 | `195 / 195 / 195` | `357 / 274 / 302` | 100,799 | `2 / 2 / 2` | исключена: потерян кластер |
| W2 | 0,20 | `146 / 146 / 146` | `264 / 206 / 222` | 76,127 | `2 / 2 / 2` | исключена: потерян кластер |
| W3 | 0,25 | `117 / 117 / 117` | `211 / 165 / 175` | 61,369 | `2 / 2 / 2` | исключена: потерян кластер |
| W4 | 0,50 | `59 / 59 / 59` | `105 / 82 / 89` | 31,291 | `1 / 2 / 2` | исключена: потеряны два кластера |
Скорость росла именно вместе с сокращением работы, но даже осторожный W1
нарушил обязательное качество на первой записи. Рабочим остался шаг `0.1`.
Комбинация C0 совпала бы с baseline, поэтому ее не запускали.
### Этап 2: можно ли удешевить модель эмбеддингов
Гипотеза: сохранить полный набор окон, но заменить WeSpeaker и при необходимости
счетчик говорящих. В M2 и M3 сохраняли `window_shift_ratio=0.1`, Pyannote
Segmentation 3.0 FP32 и последовательный инференс. Менялись модель эмбеддингов и
счетчик говорящих. Проценты ниже посчитаны относительно W0 148,220 с.
| Ячейка | Рецепт | Время, с | Изменение | RSS, МиБ | Кластеры | Решение |
|---|---|---:|---:|---:|---|---|
| W0 (рецепт M0) | WeSpeaker FP32 + `FastClustering(0.89)` | 148,220 | baseline | 362,6 | `3 / 2 / 2` | точка сравнения |
| M2 | CAMPPlus + NME + `FastClustering(N)` | 94,550 | -36,21% | 358,6 | `3 / 2 / 2` | самый быстрый, собрать C1 |
| M3 | TitaNet small + NME + `FastClustering(N)` | 107,205 | -27,67% | 511,9 | `3 / 2 / 2` | прошла, но медленнее и тяжелее M2 |
M2 и M3 прошли обязательное число кластеров. Окончательная диагностика M2
появилась в C1: purity выросла, но число слов без говорящего на первой записи
изменилось с 51 до 53. По спецификации это требует прослушивания отмеченных мест.
### Этап 3: квантизация и исполнение эмбеддингов
Для QDQ проверяли, даст ли INT8 выигрыш старому Intel без VNNI. Q1 и E1-E3
сохраняли `window_shift_ratio=0.1`, Pyannote Segmentation 3.0 FP32 и текущий
`FastClustering(0.89)`. Q1 меняла модель WeSpeaker, E1-E3 - только топологию
исполнения FP32-модели. Во всех вариантах `inter_op_threads=1`; E1-E3 использовали
общий бюджет четырех физических ядер.
| Ячейка | Рецепт | Время, с | Изменение к W0 | RSS, МиБ | Кластеры | Решение |
|---|---|---:|---:|---:|---|---|
| Q1 | WeSpeaker S8S8 QDQ, sequential | 177,537 | +19,78% | 367,7 | `2 / 2 / 2` | исключена: медленнее и потерян кластер |
| E1 | 2 outer worker, одна session, общий pool 4 | 139,219 | -6,07% | 398,6 | `3 / 2 / 2` | передать как аппаратный кандидат |
| E2 | 4 outer worker, одна session, `intra_op=1` | 152,167 | +2,66% | 551,2 | `3 / 2 / 2` | исключена по времени и памяти |
| E3 | 4 отдельные session, `intra_op=1` | 159,038 | +7,30% | 732,3 | `3 / 2 / 2` | отрицательный контроль подтвердился |
E3 измерялась составным интервалом на заранее подготовленных заданиях, поэтому
ее wall time нельзя сравнивать с `process()` так же строго, как E1/E2. Для
решения хватило отрицательного результата одновременно по времени и памяти.
TitaNet batch проверили отдельно на тех же трех матрицах:
| Контроль | Строк | Batch 1, с | Batch 4, с | Число кластеров | Метки строк совпали |
|---:|---:|---:|---:|---:|:---:|
| 1 | 531 | 27,175 | 37,588 | 3 | нет |
| 2 | 414 | 25,871 | 32,443 | 2 | нет |
| 3 | 440 | 22,877 | 28,565 | 2 | нет |
Batch 4 сохранил число кластеров, но оказался медленнее batch 1, поменял метки
строк на всех трех матрицах и дал максимальный дрейф эмбеддингов `0,277-0,317`.
Ветвь остановили до отдельной CPU-ячейки. SHA-256 закрытого batch-result:
`f12bfdb5cadf80c62871c0725eac744a99825cb5d06b70fcf11e4e538ff6df72`.
### Этап 4: комбинации перед Ryzen
Полного декартова перебора не было. C0 совпала с baseline; из оставшихся
собрали самый быстрый модельный вариант C1 и переносимый аппаратный вариант C2.
| Комбинация | Точный рецепт | Время, с | Ускорение | RSS, МиБ | Диагностика | Решение |
|---|---|---:|---:|---:|---|---|
| C1 | `0.1` + Pyannote Segmentation 3.0 FP32 + CAMPPlus + NME + `FastClustering(N)` + sequential | 98,347 | 33,65% | 399,5 | `3 / 2 / 2`; purity выше; `+2` слова без говорящего | не передавать без слуховой проверки |
| C2 | `0.1` + Pyannote Segmentation 3.0 FP32 + WeSpeaker ResNet34 LM FP32 + `FastClustering(0.89)` + 2 outer worker + 1 shared session + pool 4 | 140,632 | 4,32% | 406,0 | точное совпадение с соседним W0 | передать на Ryzen |
У C1 процент считается от разведочного W0 148,220 с. У C2 был соседний W0
146,978 с под тем же runtime guard. C2 не достигла Intel-порога продолжения,
но shared-session заранее считалась аппаратной гипотезой: Ryzen имеет восемь
физических ядер и другой ISA, поэтому спецификация требовала отдельный A/B.
## Handoff и воспроизводимость
| Объект | Значение |
|---|---|
| Merge commit в начале Ryzen-этапа | `68fda7ee893265acfa00ac78b8d4c9697f9916d9` |
| Handoff commit | `42f094ccb9464742683eec9d695e5aa9a69dd4ec` |
| Source commit эксперимента | `6345cb6449d906767b6ed33e695f2ca4eeaf3008` |
| SHA-256 Intel-отчета | `8d4183be4c3ab35f2ea043f8d87ca20e16f523cb60f9c5414d974c1bc4d6b7de` |
| SHA-256 входной Intel-капсулы | `a3a50d34783d24fe6cd13b154ca9813f9842fdcb08d99eab33a6a54efea1e3ae` |
| ID закрытого Ryzen-хранилища | `ryzen-42-20260831` |
| Upstream commit sherpa-onnx 1.13.6 | `1cb484af9b1ab05cc36f89e03dd7d76322f2e911` |
| SHA-256 патча sherpa-onnx 1.13.6 | `5f12df858cd525c8a0cfe8ff95b647c3b0e915a13d45621acc4ac42660014c49` |
| SHA-256 Intel-wheel | `306499d8668ee138357f1de43a5c1c88f4dc46c845e8a92e6ad945327bcaa050` |
| SHA-256 Ryzen-wheel | `baaaa2532606f1e1411e5c84655afa70107ad4b1676420953f5e67233c46a484` |
Входная Intel-капсула прошла полную проверку: schema v3, 199 файлов,
QDQ-артефакт доступен, предупреждений нет. Ryzen-manifest без изменений
зафиксировал данные Intel-капсулы: handoff, артефакты, контрольные записи,
calibration, QDQ, патч, версии runtime и интервал RSS. Рецепты baseline и
кандидата во всех запущенных парах совпали с переносимыми Intel-рецептами.
Ryzen-manifest, сырые результаты, рабочие артефакты стенда, guard-свидетельства
и журналы сохранены в закрытой папке Яндекс Диска. Чтобы восстановить
Ryzen-этап, нужны это хранилище и входная Intel-капсула; публичный отчет связывает
приведенные ниже восемь результатов с закрытым хранилищем только по SHA-256.
Переданный wheel не загрузился в Ryzen WSL: он требовал GLIBC 2.43, а стенд
работает с GLIBC 2.39. Для измерений wheel пересобран из того же upstream commit
и патча под текущую систему; его отдельный SHA-256 приведен в
таблице. Production-зависимости и lock-файл не менялись.
Стенд потребовал два исправления, не меняющих пользовательский контракт. Ранее
Ryzen-запуск сравнивал свой manifest побайтово с Intel `manifest.json` внутри
капсулы, хотя обязательные `stage=ryzen`, Ryzen-машина и расписание такое
совпадение исключают. Теперь Intel-manifest проверяется по индексу капсулы, а
Ryzen-manifest отдельно связывается с handoff и входами капсулы. Второе:
`temperature_celsius` допускает `null`, когда WSL и Windows не показывают
датчик температуры; отсутствие измерения больше не подменяется числом.
## Стенд Ryzen
- AMD Ryzen 7 8845H with Radeon 780M, 8 физических ядер / 16 логических
процессоров;
- 15 584 276 480 байт RAM доступны WSL, 31 944 794 112 байт установлены в
Windows; swap WSL - 4 294 967 296 байт, активность во время принятых ячеек -
нулевая;
- x86_64; AVX, AVX2, AVX-512, AVX512 VNNI, FMA, SSE4.1, SSE4.2 и AES;
- WSL2 Linux 5.15.167.4 с GLIBC 2.39, Windows 11 Enterprise 10.0.26200,
Python 3.13.13;
- `sherpa-onnx` / `sherpa-onnx-core` 1.13.6, ONNX Runtime 1.28.0,
NumPy 2.4.3, ONNX 1.22.0, kaldi-native-fbank 1.22.3, psutil 7.2.2;
- питание от сети, стабильный Windows-профиль `balanced`, EPP AC 25%,
максимальное состояние процессора 100%, boost mode `aggressive`;
- WSL не предоставляет governor; доступные интерфейсы также не показывают
температурный датчик или аппаратные лимиты мощности. Температура
записана как недоступная; это ограничивает тепловой guard;
- нативные WSL-интерфейсы не отдали supply, platform profile и throttle count,
поэтому для принятых ячеек использовали override
`LOCAL_TRANSCRIBER_RUNTIME_GUARD_JSON`, заполненный проверенными Windows-фактами:
AC, `balanced`, turbo, power policy, нулевая подкачка, отсутствие throttling.
До и после принятых пар Windows-проверка подтверждала AC, неизменный `balanced`
и отсутствие событий throttling от `Microsoft-Windows-Kernel-Processor-Power`.
Статический override сам по себе не обнаруживает изменение внутри ячейки,
поэтому внешняя проверка остается ограничением воспроизводимости;
- baseline: одна session, один outer worker, `intra_op_threads=8`,
`inter_op_threads=1`, batch 1; C2: одна shared session, два outer worker,
общий `intra_op_threads=8`, `inter_op_threads=1`, batch 1.
## Ячейки этапа 5
Время ячейки - сумма трех вызовов `diarizer.process()`, по одному на каждый
пятиминутный фрагмент. ASR и загрузка моделей в измерение не входят.
| Проверка | Порядок | Baseline, с | Кандидат, с | Ускорение | RSS baseline / кандидат, МиБ | Качество |
|---|---|---:|---:|---:|---:|:---:|
| Совпадающий baseline | W0 | 110,998 | - | - | 370,4 / - | да |
| Изолированный E1 | W0 -> E1 | 111,557 | 84,723 | 24,05% | 367,3 / 412,2 | да |
| Первичный C2 | C2 -> W0 | 111,423 | 85,909 | 22,90% | 369,6 / 410,3 | да |
E1 и C2 сохранили все обязательные и диагностические показатели baseline.
Первичный C2 прошел порог продолжения и уложился в правило памяти.
## Пять финальных A/B-циклов
| Цикл | Порядок | Baseline, с | C2, с | Парное ускорение | RSS baseline / C2, МиБ | Качество |
|---:|---|---:|---:|---:|---:|:---:|
| 1 | W0 -> C2 | 111,354 | 83,529 | 24,99% | 369,2 / 413,4 | да |
| 2 | C2 -> W0 | 108,899 | 83,907 | 22,95% | 373,0 / 418,1 | да |
| 3 | W0 -> C2 | 110,928 | 84,968 | 23,40% | 373,8 / 410,9 | да |
| 4 | C2 -> W0 | 114,985 | 83,342 | 27,52% | 376,6 / 411,1 | да |
| 5 | W0 -> C2 | 113,233 | 86,548 | 23,57% | 358,6 / 413,4 | да |
| **Итог** | - | - | - | **медиана 23,57%; MAD 0,62 п. п.** | - | **да** |
Чередование не изменило вывод: даже лучший отдельный цикл остался ниже 30%, а
медиана недобрала 6,43 процентного пункта. На Intel тот же механизм давал только
4,32%; Ryzen ускоряет shared-session заметнее, но недостаточно для `go`.
## Что уже не нужно прогонять заново
Полный replay этапов 0-5 для следующей гипотезы не нужен. Долговечное
доказательство составляют публичные агрегаты, хеши и
[карта архитектур](../research/2026-09-01-end-to-end-cpu-diarization-map.md).
Закрытые хранилища задач #41/#42 и старый материал для прослушивания C1
подлежат удалению и не являются будущей зависимостью.
| Результат | Что можно переиспользовать | Когда повтор действительно нужен |
|---|---|---|
| W1-W4 | На текущей Pyannote-модели увеличение шага теряет кластер раньше порога скорости | Изменилась segmentation-модель, логика сборки окон или обязательный quality gate |
| M2/M3 | CAMPPlus и TitaNet с NME сохраняют `3 / 2 / 2`; M2 быстрее и экономнее | Изменилась модель, счетчик, clustering или контрольные входы |
| Q1 | Этот S8S8 QDQ-граф на Intel медленнее FP32 и дает `2 / 2 / 2` | Новая калибровка, формат квантизации, модель или runtime |
| E2/E3 | Больше worker и отдельные session не окупились при прежнем бюджете потоков | Изменилась топология session, runtime или бюджет ядер |
| E1/C2 на Ryzen | Для точного рецепта shared-session медиана равна 23,57%, качество и память проходят | Изменилась реализация механизма, runtime, машина или режим питания |
| C1 | Исторический замер скорости остается доказательством незакрытого gate; старый материал для прослушивания не сохраняется | Для повторного открытия заново выполнить минимальную пару W0/C1 и подготовить новый материал для прослушивания |
Есть три разных вида проверки, и они не заменяют друг друга:
1. `--validate-only` сверяет commit, схему, хеши, handoff и план. Модели он не
запускает.
2. `uv run pytest` проверяет код стенда на mock-данных. Полный Python-suite перед
PR нужен, но он не повторяет аудиозамеры и не скачивает модели.
3. Измерительная ячейка запускает реальные модели. Для новой гипотезы нужен
только затронутый минимальный срез, один совпадающий baseline и первичный A/B.
Пять циклов и полные записи нужны лишь кандидату, прошедшему записанные пороги.
## Что осталось неизвестным
- C1 не получила слухового решения. Из автоматических метрик нельзя вывести,
допустимы ли два дополнительных слова без говорящего.
- Для восьми принятых последовательных W0 опубликовано
[суммарное разложение](../research/2026-09-01-end-to-end-cpu-diarization-map.md#измеренное-разложение-текущего-ryzen-baseline)
на segmentation, embeddings, clustering и orchestration residual. Для
shared-session C2 schema-compatible native total и orchestration residual
остаются недоступны.
- Полные записи не запускались: C2 не достигла порога; C1 не дошла до Ryzen.
- Температурный датчик в WSL и Windows недоступен. Внешняя проверка питания и
throttling снижает риск дрейфа, но не заменяет измерение температуры во время
каждой ячейки.
- Эксперимент проверял локальные замены внутри прежнего каскада. End-to-end
архитектуры в эту матрицу не входили.
## Индекс публичных доказательств
Хеши связывают обезличенные агрегаты с приватными JSON-результатами и не
раскрывают пути, текст, интервалы и идентификаторы записей.
| Результат | SHA-256 |
|---|---|
| Совпадающий baseline | `f50684784392cacc2d99c8aee51d01f0a67dfee3da6ea3ba5f3db749a3d5287a` |
| Изолированный E1 A/B | `24d5edae117e0619e432ef3f3019e8c66cf3516b87b80b64b8591390db25bf48` |
| Первичный C2 A/B | `142db6d06efc8d558b38e18f47f47e3e35e7df00751da2583339f99add8d75c4` |
| Финальный цикл 1 | `4b5095c9e64ff0ef75bbc58516c662bedbbdb0bedf131f66cc1031c9a0ad8c7f` |
| Финальный цикл 2 | `256a17b2d08510aad03d43f5c626c1a4269c9a25271ce492dc3305c4afca1888` |
| Финальный цикл 3 | `b5d37c7a8b05b065c61bc9d5f1c0ab639a80a821730e505179388d90c4239caa` |
| Финальный цикл 4 | `639cc9f13511a59c7240c6fb6ec6bf1c31ec26350330e3ca0c64fb721b768736` |
| Финальный цикл 5 | `4ff2863a93fc22a037de5c292391b6b46de1b539cf397026b349ea5134cf1051` |
## Решение
Итог - `new map`:
1. C2 прошла обязательное качество и память.
2. Медианное ускорение 23,57% не достигло порога 30%.
3. W1-W4 потеряли кластер, а C1 преодолела 30% на Intel только с незакрытым
диагностическим слуховым gate: сокращение окон и цены эмбеддинга уперлось в
качество раньше итогового порога.
4. По правилу спецификации следующий шаг - отдельная карта end-to-end
диаризации: [карта архитектур](../research/2026-09-01-end-to-end-cpu-diarization-map.md)
и [контракт Streaming Sortformer](../specs/2026-09-01-streaming-sortformer-cpu-experiment.md).
Карта подготовлена в
[задаче #45](https://git.dementev.space/ddmitry/local-transcriber/issues/45),
а контракт исполняет
[задача #46](https://git.dementev.space/ddmitry/local-transcriber/issues/46).
5. Полные записи и Ryzen-слуховая приемка не требуются без победителя.
6. Production-профиль, CLI, конфигурационный контракт и модель по умолчанию
остаются без изменений.