diff --git a/docs/benchmarks/2026-08-31-cpu-diarization-ryzen.md b/docs/benchmarks/2026-08-31-cpu-diarization-ryzen.md index 46edb03..f6887bd 100644 --- a/docs/benchmarks/2026-08-31-cpu-diarization-ryzen.md +++ b/docs/benchmarks/2026-08-31-cpu-diarization-ryzen.md @@ -202,7 +202,7 @@ Ryzen-manifest отдельно связывается с handoff и входа 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, а доступные интерфейсы не показывают +- WSL не предоставляет governor; доступные интерфейсы также не показывают температурный датчик или аппаратные лимиты мощности. Температура записана как недоступная; это ограничивает тепловой guard; - нативные WSL-интерфейсы не отдали supply, platform profile и throttle count, @@ -248,8 +248,11 @@ E1 и C2 сохранили все обязательные и диагност ## Что уже не нужно прогонять заново -Полный replay этапов 0-5 для следующей гипотезы не нужен. Результаты закреплены -публичными агрегатами, хешами и закрытой капсулой. +Полный replay этапов 0-5 для следующей гипотезы не нужен. Долговечное +доказательство составляют публичные агрегаты, хеши и +[карта архитектур](../research/2026-09-01-end-to-end-cpu-diarization-map.md). +Закрытые хранилища задач #41/#42 и старый материал для прослушивания C1 +подлежат удалению и не являются будущей зависимостью. | Результат | Что можно переиспользовать | Когда повтор действительно нужен | |---|---|---| @@ -258,7 +261,7 @@ E1 и C2 сохранили все обязательные и диагност | 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 | Сначала прослушать подготовленные места; новый CPU-замер нужен лишь после положительного решения | +| C1 | Исторический замер скорости остается доказательством незакрытого gate; старый материал для прослушивания не сохраняется | Для повторного открытия заново выполнить минимальную пару W0/C1 и подготовить новый материал для прослушивания | Есть три разных вида проверки, и они не заменяют друг друга: @@ -274,10 +277,12 @@ E1 и C2 сохранили все обязательные и диагност - C1 не получила слухового решения. Из автоматических метрик нельзя вывести, допустимы ли два дополнительных слова без говорящего. -- На Ryzen не опубликовано разложение wall time на segmentation, embeddings, - clustering и orchestration. По общему времени нельзя надежно назвать следующий - доминирующий компонент. -- Полные записи не запускались: C2 не достигла порога, а C1 не дошла до Ryzen. +- Для восьми принятых последовательных 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 снижает риск дрейфа, но не заменяет измерение температуры во время каждой ячейки. @@ -310,9 +315,12 @@ E1 и C2 сохранили все обязательные и диагност диагностическим слуховым gate: сокращение окон и цены эмбеддинга уперлось в качество раньше итогового порога. 4. По правилу спецификации следующий шаг - отдельная карта end-to-end - диаризации, заведенная как - [задача #45](https://git.dementev.space/ddmitry/local-transcriber/issues/45). - Ее гипотезы и замеры относятся к отдельному эксперименту. + диаризации: [карта архитектур](../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, конфигурационный контракт и модель по умолчанию остаются без изменений. diff --git a/docs/research/2026-09-01-end-to-end-cpu-diarization-map.md b/docs/research/2026-09-01-end-to-end-cpu-diarization-map.md new file mode 100644 index 0000000..50435d5 --- /dev/null +++ b/docs/research/2026-09-01-end-to-end-cpu-diarization-map.md @@ -0,0 +1,436 @@ +# Карта end-to-end архитектур CPU-диаризации + +**Дата:** 2026-09-01 + +**Статус:** выбран один кандидат для свежего минимального A/B - Streaming +Sortformer 4spk v2 Q8_0 через закрепленный NeMo-Speech.cpp. Это единственная +ветвь, где полная замена каскада опирается на готовые Q8_0 веса и закрепляемый +локальный CPU runtime. Production-профиль, CLI, конфигурационный контракт и +модель по умолчанию не меняются. + +Документ сохраняет обезличенные агрегаты задач #41 и #42, потому что после +удаления закрытых хранилищ их нельзя будет получить повторно без нового +вычисления. Здесь нет локальных путей, идентификаторов записей, текста, +временных интервалов, manifest-содержимого или сырых строк результата. +Непрозрачные SHA-256 уже опубликованных результатов оставлены как устойчивые +ссылки на исторические байты. Они не создают будущую зависимость от этих байтов. + +## Источники реестра + +- [Intel-отчет](../benchmarks/2026-08-31-cpu-diarization-intel.md); +- [Ryzen-отчет](../benchmarks/2026-08-31-cpu-diarization-ryzen.md); +- [спецификация прежнего эксперимента](../specs/2026-08-30-cpu-diarization-experiment.md); +- [ADR-007](../adr/007-word-level-speaker-diarization.md). + +## Как читать реестр + +`Ускорение` ниже означает сокращение wall time: + +`100 x (wall_baseline - wall_candidate) / wall_baseline`. + +Отрицательное значение означает замедление. Intel W/M/Q/E - одиночные +разведочные прогоны трех 300-секундных фрагментов, если явно не указана соседняя +пара. Ryzen-строки - принятые соседние A/B. ASR, загрузка моделей и декодирование +не входят в wall. + +Обязательный автоматический gate: `3 / 2 / 2` голосовых кластера, неизменный +текст, повторяемые параметры/метки/финализация, конечные эмбеддинги и отсутствие +потерянных или продублированных входов. `51 / 13 / 9` слов без говорящего и +`mapped speaker purity = 0,76655 / 0,78742` - диагностика совпадающего baseline, +а не ручная истина. Рост диагностического показателя требует прослушивания, но +сам по себе не превращает вариант в проигравший. + +## Переносимые рецепты + +Все Intel-рецепты используют четыре физических ядра; Ryzen-рецепты - восемь. +Везде `inter_op_threads=1`, batch 1, если строка не говорит обратного. +Код измерительного стенда закреплен source commit +`6345cb6449d906767b6ed33e695f2ca4eeaf3008`; sherpa-onnx 1.13.6 - upstream +commit `1cb484af9b1ab05cc36f89e03dd7d76322f2e911` плюс опубликованный ниже патч. +Последовательные W0-рецепты используют CPU provider для segmentation и +embeddings, `debug=True` для обеих конфигураций, +`min_duration_on=0.3` и `min_duration_off=0.5`. Debug-режим входит в +измерительный рецепт: из его журнала получены native stage times. + +| Recipe ID | Точный переносимый рецепт | +|---|---| +| `I-B135` | `sherpa-onnx/core 1.13.5`; Pyannote Segmentation 3.0 FP32; WeSpeaker ResNet34 LM FP32; `window_shift_ratio=0.1`; последовательный инференс; один worker/session; `intra_op=4`; `FastClustering`, auto-N, threshold `0.89`. | +| `I-B136` / `I-W0` / `I-M0` / `I-Q0` / `I-E0` / `I-C0` | Тот же профиль, но `sherpa-onnx/core 1.13.6`; это совпадающий Intel baseline. | +| `I-W(r)` | `I-B136`, меняется только `window_shift_ratio=r`. | +| `I-M2` / `I-C1` | `I-B136`, но CAMPPlus вместо WeSpeaker; NME автоматически выбрал `p=111/54/104`, затем N=`3/2/2` и `FastClustering(N)`; sequential. | +| `I-M3` | `I-B136`, но TitaNet small вместо WeSpeaker; NME автоматически выбрал `p=90/65/110`, затем N=`3/2/2` и `FastClustering(N)`; sequential. | +| `I-Q1` | `I-B136`, но WeSpeaker S8S8 QDQ вместо FP32; sequential. | +| `I-E1` / `I-C2` | `I-B136`, но два outer worker используют одну shared ONNX Runtime session и общий `intra_op=4`. | +| `I-E2` | `I-B136`, но четыре outer worker, одна shared session, `intra_op=1`. | +| `I-E3` | `I-B136`, но четыре outer worker и четыре отдельные session, у каждой `intra_op=1`; отрицательный контроль. | +| `R-B` | `sherpa-onnx/core 1.13.6`; Pyannote FP32; WeSpeaker FP32; shift `0.1`; `FastClustering(auto, 0.89)`; sequential; один worker/session; `intra_op=8`. | +| `R-C2` | `R-B`, но два outer worker, одна shared session, общий `intra_op=8`. | + +Закрепленные SHA-256 моделей: Pyannote FP32 +`220ad67ca923bef2fa91f2390c786097bf305bceb5e261d4af67b38e938e1079`, +WeSpeaker FP32 +`e9848563da86f263117134dfd7ad63c92355b37de492b55e325400c9d9c39012`, +WeSpeaker QDQ +`9a3f8ae64d1083b64a6276f1e1138b17b7511a9a8d731b632945efc84098588b`, +CAMPPlus +`aa3cfc16963a10586a9393f5035d6d6b57e98d358b347f80c2a30bf4f00ceba2`, +TitaNet small +`ad4a1802485d8b34c722d2a9d04249662f2ece5d28a7a039063ca22f515a789e`. +Экспериментальные `Prepare/Finalize` и shared worker требовали патч +`5f12df858cd525c8a0cfe8ff95b647c3b0e915a13d45621acc4ac42660014c49`: +API не было ни в 1.13.6, ни в проверенном upstream master. Ryzen-wheel был +пересобран из того же upstream commit и патча из-за несовместимости GLIBC; +его опубликованный SHA-256 - +`baaaa2532606f1e1411e5c84655afa70107ad4b1676420953f5e67233c46a484`. + +## Реестр: baselines и проверки механизмов + +| Stable ID | Recipe / измененная ось | Машина / стадия | Wall / ускорение / RSS | Обязательное качество | Решение и точная причина | Публичное доказательство | +|---|---|---|---|---|---|---| +| `41-V135` | `I-B135`; version baseline | Intel / version baseline | `190,104 с`; baseline; `365,7 МиБ` в соответствующем сыром агрегате | `3/2/2`, текст и диагностика baseline | Принят как контроль версии; одиночный запуск не доказывает причинное ускорение/замедление версии | `docs/benchmarks/2026-08-31-cpu-diarization-intel.md`, "Зачем понадобился 1.13.6" | +| `41-V136` | `I-B136`; меняется только runtime 1.13.5->1.13.6 | Intel / version baseline | `157,892 с`; `+16,94%` к `41-V135`; `356,7 МиБ` | `3/2/2`; канонические интервалы, назначения слов и диагностика совпали | Baseline 1.13.6 принят; разница времени диагностическая, без повторов | тот же раздел; SHA `eb7e3d508836e1eefd679a851271eaa6823d688fef39d2ae03978e7fa7006bba` | +| `41-S-WINDOW` | 1.13.6 принимает shift `0.10/0.15/0.20/0.25/0.50`; `0.10` сохраняет поведение | Intel / smoke | end-to-end wall/RSS: not measured | Smoke пройден | Допустить W0-W4 | `docs/specs/2026-08-30-cpu-diarization-experiment.md`, этап 0 | +| `41-S-PF` | Локальный `Prepare -> cluster(copy) -> Finalize` против штатного `process()` | Intel / smoke | wall/ускорение/RSS: not measured | Строки, метки и интервалы совпали; пропусков/дублей нет | Использовать патч в стенде | Intel-отчет, этап 0 | +| `41-S-MANIFEST` | Строгая schema v3 и стабильные ID ячеек | Intel / smoke | не применимо | Валидация капсулы: 199 файлов, QDQ доступен, предупреждений нет | Использовать; это механизм воспроизводимости, не измерительная ячейка | Ryzen-отчет, handoff | +| `41-S-CEIG` | CAMPPlus prepared-матрицы; eigengap | Intel / counter smoke | counter wall по трем матрицам `0,107 / 1,575 / 0,067 с`; end-to-end ускорение/RSS: not measured | N=`6/1/7`, не `3/2/2` | Исключить счетчик для CAMPPlus: обязательный count gate провален | Новый aggregate задачи #45 из frozen raw; previous result SHA not published | +| `41-S-CNME` | Те же CAMPPlus-матрицы; NME + `FastClustering(N)` | Intel / counter smoke | counter wall `3,536 / 1,488 / 1,148 с`; end-to-end ускорение/RSS: not measured | N=`3/2/2`, повторяемые метки, 0 потерянных строк | Выбрать для M2/C1 | Intel-отчет, индекс доказательств | +| `41-S-TEIG` | TitaNet prepared-матрицы; eigengap | Intel / counter smoke | counter wall `0,126 / 0,073 / 0,101 с`; end-to-end ускорение/RSS: not measured | N=`7/2/4`, не `3/2/2` | Исключить счетчик для TitaNet: count gate провален | Новый aggregate задачи #45 из frozen raw; previous result SHA not published | +| `41-S-TNME` | Те же TitaNet-матрицы; NME + `FastClustering(N)` | Intel / counter smoke | counter wall `1,084 / 0,780 / 1,112 с`; end-to-end ускорение/RSS: not measured | N=`3/2/2`, повторяемые метки, 0 потерянных строк | Выбрать для M3 | Intel-отчет, индекс доказательств | +| `41-S-QDQ` | WeSpeaker S8S8 QDQ, 48 независимых calibration-интервалов | Intel / artifact smoke | wall/ускорение/RSS: not measured | interface/opset/metadata, checker, dynamic shapes и загрузка sherpa пройдены | Допустить Q1; техническая корректность не означает качество/скорость | Intel-отчет, этап 0; model SHA выше | +| `41-S-SHARED` | Один и два worker, одна session, одинаковые prepared-входы | Intel / worker smoke | wall/ускорение/RSS: not measured | строки, метки и интервалы совпали; гонок/NaN/потерь нет | Допустить E1/E2 | Ryzen-отчет, этап 0 | +| `41-S-TB1` | TitaNet batch 1 | Intel / embedding smoke | embedding-only `75,923 с` суммарно; ускорение/RSS: not measured | штатные эмбеддинги воспроизведены, max abs drift <= `3,4e-6` | Контроль batch | Ryzen-отчет, этап 3; batch SHA `f12bfdb5cadf80c62871c0725eac744a99825cb5d06b70fcf11e4e538ff6df72` | +| `41-S-TB4` | TitaNet batch 4 | Intel / embedding smoke | embedding-only `98,596 с`, на `29,86%` медленнее batch 1; RSS: not measured | `3/2/2`, но метки всех трех матриц изменились; drift `0,277-0,317` | Остановить до CPU-ячейки: медленнее и нарушена идентичность меток | Ryzen-отчет, этап 3; тот же batch SHA | + +Counter wall в строках `41-S-CEIG` и `41-S-TEIG` впервые опубликован в задаче +#45 как безопасный aggregate frozen raw evidence. Прежний result SHA не был +опубликован; после удаления хранилища эти числа нельзя повторно сверить с +исходными байтами. + +Примечание к `41-V135`: публичный Intel-индекс печатает SHA +`c53c1f970ee4db094779bc32b42f978c37c2cb74d814504ccfac87f09e802ddd`, +но этот хеш связывает другой сырой агрегат (`148,806 с`), а не опубликованные +`190,104 с`. Сырой результат с `190,104 с` имеет неопубликованный хеш. Поэтому +для этой строки устойчивым публичным доказательством остается только якорь +публичного отчета; после удаления хранилища байтовая проверка числа невозможна. + +## Реестр: Intel W/M/Q/E/C + +| Stable ID | Recipe / измененная ось | Wall / ускорение / RSS | Обязательное качество и диагностика | Решение и точная причина | Публичное доказательство | +|---|---|---|---|---|---| +| `41-W0` | `I-W(0.10)`, совпадающий baseline | `148,220 с`; baseline; `361,2 МиБ` в том же raw result | pass `3/2/2`; слова без говорящего `51/13/9` | Принять shift `0.10` | SHA `32d513cd0e81fdca0e37484f86b5c4312d3573608493972a282302dd891cdf0e` | +| `41-W1` | `I-W(0.15)`; меняется только shift | `100,799 с`; `+31,99%`; `362,6 МиБ` | fail `2/2/2`; `50/17/9` | Исключить: потерян кластер на первом фрагменте | SHA `be8935b5a81e2aadaeb38540e615c0cd3ae5ac31d6e0949856fd7d16d31a78ea` | +| `41-W2` | `I-W(0.20)` | `76,127 с`; `+48,64%`; `363,6 МиБ` | fail `2/2/2`; `52/13/9` | Исключить: потерян кластер | SHA `f95433d516772d40c4071da0d0745b981314ee7a7063fb4a43518e94bc17848e` | +| `41-W3` | `I-W(0.25)` | `61,369 с`; `+58,60%`; `358,4 МиБ` | fail `2/2/2`; `59/16/11` | Исключить: потерян кластер | SHA `14cac4f1d8cd38ed5b4a20189438d2b1e109ba267c5feaa36f0928fbec848580` | +| `41-W4` | `I-W(0.50)` | `31,291 с`; `+78,89%`; `360,7 МиБ` | fail `1/2/2`; `0/19/10` | Исключить: потеряны два кластера суммарно относительно `3/2/2` | SHA `6fafefce00e90590ab2d834fb560413cd7780e20cbd1f631d9954ec8b97a540e` | +| `41-M0` | `I-M0`; baseline модельной оси | Переиспользован `41-W0`: `148,220 с`; `361,2 МиБ`; отдельное ускорение: not measured | Качество baseline прошло | Точка сравнения; не новая гипотеза | Ryzen-отчет, этап 2; SHA W0 выше | +| `41-M2` | `I-M2`; меняются embedding model и estimator N | `94,550 с`; `+36,21%`; `358,6 МиБ` | Обязательный gate пройден: `3/2/2`; `53/13/9` | Самая быстрая модельная ветвь; собрать C1, но диагностический рост требует слухового gate | SHA `3debc8ae5cf1bd13d806dbfe8e4cfa137d6705622a721f4a42f1a22c030ec277` | +| `41-M3` | `I-M3`; меняются embedding model и estimator N | `107,205 с`; `+27,67%`; `511,9 МиБ` | Обязательный gate пройден: `3/2/2`; `52/13/9` | Не выбирать: медленнее и тяжелее M2; обязательный gate при этом пройден | SHA `531a842234e35e29f582ff8a51406fb9dd6211e017b3162d80cca21d4732d316` | +| `41-Q0` | `I-Q0`; baseline quantization-оси | Переиспользован `41-W0`; отдельный raw aggregate: not published | Качество baseline прошло | Точка сравнения; не новая гипотеза | Старая спецификация, QDQ | +| `41-Q1` | `I-Q1`; FP32->S8S8 QDQ | `177,537 с`; `-19,78%` (замедление); `367,7 МиБ` | `fail`: `2/2/2`; `51/13/8` | Исключить: медленнее и потерян кластер | Intel-отчет, этапы 2-3; result SHA not published | +| `41-Q-U8-SKIP` | Условный U8U8; меняется формат квантизации; Intel / этап 3 | wall/ускорение/RSS: not measured | Качество: not measured | Ветка не открылась: S8S8 не достиг 10%-го порога продолжения и провалил обязательное качество | Старая спецификация, этап 3; result SHA not published | +| `41-E0a` | `I-E0`; companion baseline для E1 | Отдельные wall/ускорение/RSS: not published; отдельный result blob: not published; сравнительный W0=`148,220 с` | Качество baseline прошло | Служебная baseline-ячейка; самостоятельного решения нет | Intel-отчет, таблица ID ячеек | +| `41-E1` | `I-E1`; topology 1 worker->2 shared workers | `139,219 с`; `+6,07%`; `398,6 МиБ` | Качество прошло: `3/2/2`; метки/интервалы/диагностика совпали | Ниже Intel-порога продолжения, но допущен как заранее оговоренная ISA-гипотеза для собственного Ryzen A/B | SHA `f5ac41d29eeff1f7cfbb93497a0025aef40de0838e933e0f9baed30a236d6b85` | +| `41-E0b` | `I-E0`; companion baseline для E2 | Отдельные wall/ускорение/RSS: not published; сравнительный W0=`148,220 с` | Качество baseline прошло | Служебная baseline-ячейка | Intel-отчет, таблица ID ячеек | +| `41-E2` | `I-E2`; 4 shared workers, `intra_op=1` | `152,167 с`; `-2,66%`; `551,2 МиБ` | pass `3/2/2`, точное совпадение результата | Исключить: медленнее и больше RSS | SHA `f2664d354ede1e4bf2c42b974513cdc845ff601c73f30ebae51456764fd8dcdc` | +| `41-E0c` | `I-E0`; companion baseline для E3 | Отдельные wall/ускорение/RSS: not published; сравнительный W0=`148,220 с` | Качество baseline прошло | Служебная baseline-ячейка | Intel-отчет, таблица ID ячеек | +| `41-E3` | `I-E3`; shared->4 separate session | `159,038 с` составная оценка; `-7,30%`; `732,3 МиБ` process-wide | `pass`: `3/2/2`; метки совпали | Исключить: отрицательный контроль подтвердил рост времени и памяти; wall не равноправен штатному `process()` | Intel-отчет, этапы 2-3; result SHA not published | +| `41-C0` | `I-C0`; рабочий shift + baseline stack | wall/ускорение/RSS: not measured | по определению совпадает с прошедшим W0 | Явно пропущена как дубликат baseline | Intel-отчет, этап 1/4 | +| `41-C1` | `I-C1`; финальная проверка сильнейшей Intel-ветви | `98,347 с`; `+33,65%` к одиночному W0; `399,5 МиБ` | Обязательный gate пройден: `3/2/2`, purity `0,80137/0,91079`; слова `53/13/9` (`+2` на первом фрагменте) | **Неразрешенный слуховой gate: ни победитель, ни проигравший.** Не передавать до прослушивания подготовленных мест | SHA `4f96e6f7363e0edb79d4034a07f4d3b6b0003a820fcf6bceff5426bb3c2caff4` | +| `41-W0C2-SWAP` | `I-B136`; первая попытка guarded baseline; Intel / финальная пара | wall/ускорение/RSS: not published | Качество: not published | Отклонить: runtime guard обнаружил swap-in | Intel-отчет, раздел "Условия"; result SHA not published | +| `41-W0C2` | `I-B136`; соседний guarded baseline | `146,978 с`; baseline; `376,4 МиБ` | Качество прошло; диагностика baseline | Допустимая база сравнения только для `41-C2` | SHA `e572dd057ec87514aca4617465c48ce361677ae19279efcadbbde404afc85b5d` | +| `41-C2` | `I-C2`; baseline->2 shared workers | `140,632 с`; `+4,32%`; `406,0 МиБ` | Обязательная и диагностическая проверка точно совпала с `41-W0C2` | Intel-порог не достигнут; передать как работоспособную аппаратную ISA-гипотезу по явному правилу спецификации | SHA `d687074cc582e2ac379ed3375362f9480778feca02c7492a4c4be4fcc1a4f15a` | + +`M1` не был ячейкой согласованной матрицы: нумерация spec - M0, M2, M3. + +В публичной сводке для W0/M0 указан RSS `362,6 МиБ`, однако опубликованный +SHA W0 связывает wall `148,220 с` с RSS `361,2 МиБ`. В реестре выше сохранена +согласованная пара из одного raw result; публичные `362,6 МиБ` нельзя привязать +к тому же опубликованному SHA. + +## Реестр: Ryzen, этапы 5-6 + +Для каждой A/B-строки recipe baseline - `R-B`, candidate - `R-C2`; меняется +только topology embedding-инференса. Все ячейки прошли обязательный gate и +сохранили baseline-диагностику (`3/2/2`, `51/13/9`, та же purity, те же числа +окон/embedding jobs, без потерь или дублей). + +| Stable ID | Машина / стадия / порядок | Wall baseline -> candidate / ускорение | RSS baseline -> candidate | Решение и точная причина | Published result SHA-256 | +|---|---|---|---|---|---| +| `42-W0` | Ryzen / matching baseline | `110,998 с`; candidate: not measured | `370,4 МиБ`; candidate: not measured | Baseline принят | `f50684784392cacc2d99c8aee51d01f0a67dfee3da6ea3ba5f3db749a3d5287a` | +| `42-E1-AB` | Ryzen / isolated mechanism / W0->E1 | `111,557 -> 84,723 с`; `24,05%` | `367,3 -> 412,2 МиБ` | Механизм прошел качество, память и 10%-й порог продолжения | `24d5edae117e0619e432ef3f3019e8c66cf3516b87b80b64b8591390db25bf48` | +| `42-C2-P` | Ryzen / primary / C2->W0 | `111,423 -> 85,909 с`; `22,90%` | `369,6 -> 410,3 МиБ` | Прошел primary-порог; допустить пять циклов | `142db6d06efc8d558b38e18f47f47e3e35e7df00751da2583339f99add8d75c4` | +| `42-C2-F1` | Ryzen / final 1 / W0->C2 | `111,354 -> 83,529 с`; `24,99%` | `369,2 -> 413,4 МиБ` | Принятый цикл; качество pass | `4b5095c9e64ff0ef75bbc58516c662bedbbdb0bedf131f66cc1031c9a0ad8c7f` | +| `42-C2-F2` | Ryzen / final 2 / C2->W0 | `108,899 -> 83,907 с`; `22,95%` | `373,0 -> 418,1 МиБ` | Принятый цикл; качество pass | `256a17b2d08510aad03d43f5c626c1a4269c9a25271ce492dc3305c4afca1888` | +| `42-C2-F3` | Ryzen / final 3 / W0->C2 | `110,928 -> 84,968 с`; `23,40%` | `373,8 -> 410,9 МиБ` | Принятый цикл; качество pass | `b5d37c7a8b05b065c61bc9d5f1c0ab639a80a821730e505179388d90c4239caa` | +| `42-C2-F4` | Ryzen / final 4 / C2->W0 | `114,985 -> 83,342 с`; `27,52%` | `376,6 -> 411,1 МиБ` | Принятый цикл; качество pass | `639cc9f13511a59c7240c6fb6ec6bf1c31ec26350330e3ca0c64fb721b768736` | +| `42-C2-F5` | Ryzen / final 5 / W0->C2 | `113,233 -> 86,548 с`; `23,57%` | `358,6 -> 413,4 МиБ` | Принятый цикл; качество pass | `4ff2863a93fc22a037de5c292391b6b46de1b539cf397026b349ea5134cf1051` | +| `42-STAGE6-SKIP` | Ryzen / этап 6 / `R-B` и `R-C2` на трех полных записях | wall/ускорение: not measured | RSS: not measured | Этап пропущен: медиана финальных циклов `23,57%` ниже порога `30%`; качество: not measured | Ryzen-отчет, "Итог"; result SHA not published | + +Итог C2: медиана сокращения `23,57%`, MAD `0,62 п. п.`; до порога `go` +`30%` не хватило `6,43 п. п.`. Качество и память прошли, скорость - нет; +решение всей воронки `new map`. Stage 6 (полные записи и пятичастная слуховая +приемка) не запускался, потому что победителя нет. + +## Измеренное разложение текущего Ryzen baseline + +### Семантика полей + +В непатченном последовательном baseline outer `wall_seconds` измеряется вокруг +одного `diarizer.process(samples)`. Native debug отдельно дает +`segmentation_seconds`, `embedding_seconds`, `clustering_seconds` и +`engine_total_seconds`. Поэтому для каждого из трех фрагментов, а затем для их +суммы, допустимо вычислить: + +`orchestration = measured outer wall - native engine total`. + +Это остаток границы Python/native плюс progress callback и измерительный +обвес; схема не позволяет разложить его дальше и не доказывает, что весь остаток +принадлежит production-коду. Native stage и native total напечатаны с точностью +до `0,001 с`; таблица округляет итоговые суммы до `0,001 с`. Разность использует +сохраненные числа, до отображения не округляется. + +У патченного shared-session runner `engine_total_seconds` намеренно записан +равным outer wall всего `Prepare + counter + clustering + Finalize`. Это не +отдельный native total. Поэтому для E1/C2 нельзя честно вычислить orchestration +как `outer - native`: соответствующая величина **недоступна**, а ноль был бы +артефактом схемы. + +### Сумма трех записей в каждой принятой baseline-ячейке + +| Baseline ID | Outer wall, с | Segmentation, с | Embeddings, с | Clustering, с | Native engine total, с | Orchestration, с | +|---|---:|---:|---:|---:|---:|---:| +| `42-W0` | 110,998 | 18,233 | 82,172 | 0,015 | 100,444 | 10,554 | +| `42-E1-AB/W0` | 111,557 | 17,888 | 82,952 | 0,015 | 100,871 | 10,686 | +| `42-C2-P/W0` | 111,423 | 19,685 | 83,635 | 0,014 | 103,357 | 8,066 | +| `42-C2-F1/W0` | 111,354 | 18,614 | 84,596 | 0,015 | 103,240 | 8,114 | +| `42-C2-F2/W0` | 108,899 | 21,071 | 79,911 | 0,012 | 101,010 | 7,889 | +| `42-C2-F3/W0` | 110,928 | 21,204 | 81,722 | 0,016 | 102,960 | 7,968 | +| `42-C2-F4/W0` | 114,985 | 19,594 | 84,715 | 0,013 | 104,342 | 10,643 | +| `42-C2-F5/W0` | 113,233 | 21,625 | 82,884 | 0,013 | 104,540 | 8,693 | +| **Суммарно: 8 ячеек / 24 записи** | **893,377** | **157,914** | **662,587** | **0,113** | **820,764** | **72,613** | + +Stage-суммы отличаются от native total pooled на `0,150 с` (`0,017%` outer +wall): это ожидаемый остаток тысячного округления native debug, а не оцененная +пятая стадия. + +Медианы по восьми matching baselines, рассчитанные из показанных значений без +промежуточного округления: outer `111,388 с`, segmentation `19,6395 с`, +embeddings `82,918 с`, clustering `0,0145 с`, native total +`103,100 с`, orchestration `8,404 с`. + +Наблюдаемый spread (max-min, процент относительно min): + +| Величина | Диапазон, с | Spread | +|---|---:|---:| +| Outer wall | `108,899-114,985` | `6,087 с` / `5,59%` | +| Segmentation | `17,888-21,625` | `3,737 с` / `20,89%` | +| Embeddings | `79,911-84,715` | `4,804 с` / `6,01%` | +| Clustering | `0,012-0,016` | `0,004 с`; относительный процент неинформативен при гранулярности `0,001 с` | +| Native total | `100,444-104,540` | `4,096 с` / `4,08%` | +| Orchestration residual | `7,889-10,686` | `2,797 с` / `35,45%` | + +Это объясняет, почему Intel-разница W0/C2 `4,32%` неотличима от наблюдавшегося +там разброса, а Ryzen-финал опирался на парные циклы, а не на один baseline. + +## Доли, проверенные механизмы и Amdahl ceiling + +Доли ниже - pooled stage time / pooled outer wall восьми matching Ryzen +baseline. Amdahl ceiling предполагает невозможное идеальное удаление ровно +одного компонента при неизменности остальных: + +`max speedup = 1 / (1 - share)`. + +| Компонент | Измеренная доля current Ryzen wall | Уже проверенные сокращения | Что осталось после эксперимента | Теоретический ceiling | +|---|---:|---|---|---:| +| Segmentation | `17,676%` | W1-W4 уменьшали число окон `291->195/146/117/59` на каждом фрагменте и вместе число embedding jobs; wall падал до `31,291 с`, но уже W1 потерял обязательный кластер | Без новой segmentation-модели/логики окон дальнейшее увеличение shift закрыто quality gate | максимум `1,2147x` (`17,676%` сокращения wall) при полном устранении стадии | +| Embeddings | `74,167%` | CAMPPlus+NME (`36,21%` Intel, слуховой gate C1 открыт), TitaNet+NME (`27,67%`, тяжелее M2), QDQ (медленнее и fail), shared session (Ryzen median `23,57%`), 4 workers/separate sessions (хуже), TitaNet batch 4 (медленнее и другие метки) | Это доминирующая измеренная стадия; локальные замены прежнего каскада не дали принятого 30%-победителя | максимум `3,8709x` (`74,167%` wall) при полном устранении стадии | +| Clustering | `0,013%` | Eigengap быстро, но неверный N; NME дал правильный N для CAMPPlus/TitaNet, однако его отдельный counter wall измеряется секундами и не является baseline native clustering time | Ускорять baseline `FastClustering` бессмысленно по Amdahl; менять алгоритм можно только ради качества/архитектуры | максимум `1,00013x` (`0,013%` wall) | +| Orchestration residual | `8,128%` | `Prepare/Finalize` и shared worker прошли correctness smoke; отдельного эксперимента на сокращение остатка не было | Нельзя отделить project orchestration от callback/RSS/harness; candidate-side доля недоступна из текущей схемы | максимум `1,0885x` (`8,128%` wall) при полном устранении остатка | + +Сумма четырех долей `99,983%`; недостающие `0,017%` - только эффект +округления native stage logs. Нельзя складывать наблюденные ускорения разных +ячеек как независимые: W-ветвь одновременно сокращала segmentation и число +embedding jobs, а shared-session меняла исполнение embedding внутри патченного +runner. + +## Открытые gates и неизмеренные факты + +1. **C1 остается неразрешенным слуховым gate - ни победитель, ни проигравший.** + Обязательная автоматическая проверка прошла, скорость на Intel превысила + 30%, purity выросла, но два + дополнительных слова без говорящего требуют человеческого прослушивания. + Автоматические метрики не разрешают этот выбор. +2. Полные записи не измерялись ни для C1, ни для C2. C1 не дошла до Ryzen, C2 + не прошла финальный порог скорости. +3. Ryzen C1 wall/RSS/quality неизвестны полностью; переносить Intel `33,65%` на + другую ISA нельзя. +4. Для shared-session E1/C2 нет schema-compatible native total и, следовательно, + нет candidate-side orchestration share. Доступные segmentation/embedding/ + clustering поля не закрывают остаток `counter + Finalize + Python` отдельными + сопоставимыми стадиями. +5. Orchestration baseline - измеренный residual, но его production/harness + состав не измерен. +6. Температура Ryzen во время ячеек недоступна. Внешний power/throttle + guard прошел, но не заменяет сенсор внутри WSL. +7. Intel W/M/Q/E разведка - по одному прогону, ранние ячейки не чередовались; + их проценты не являются оценкой дисперсии. +8. End-to-end архитектуры не входили в матрицу. Измерены только локальные + изменения прежнего каскада. +9. `M1` отсутствует по дизайну, C0 пропущена как точный baseline-дубликат, + stage 6 пропущен по правилу "нет победителя"; это известные пропуски, а не + потерянные результаты. +10. Отдельные агрегаты Intel companion-ячеек M0/Q0/E0 не опубликованы. Их можно + использовать только как recipe-alias W0, а не выдавать за новые замеры. + +## Что станет невосстановимым после удаления замороженных хранилищ + +Долговечная карта должна сохранить все таблицы и формулы выше. После удаления +обоих хранилищ останутся публичные рецепты, агрегаты, решения и непрозрачные +хеши, но сами хеши уже нельзя будет повторно сверить с исходными байтами. + +Без нового model run или повторной подготовки будут потеряны: + +- все raw per-recording outer/stage/CPU/RSS значения за пределами опубликованных + агрегатов, включая точное восьмиклеточное Ryzen-разложение; +- сырые интервалы разметки говорящих, назначения слов, диагностические records, + ASR-sidecars и опорные материалы; +- prepared embedding-матрицы, row mappings, промежуточные результаты счетчиков, + worker requests и native logs, которыми проверялись строки/метки/финализация; +- runtime-guard свидетельства и полное состояние планировщика/manifest, включая + непубличные идентификаторы ячеек и порядок внутренних повторов; +- подготовленные C1 listening excerpts и plan. Если gate не закрыть до удаления, + для его будущего решения придется заново построить минимальный baseline/C1 + diff и новые excerpts из исходных записей; +- calibration provenance и байты QDQ-графа, локальные wheel/patch artifacts, + если они отдельно не сохранены; опубликованный SHA доказывает идентичность + только при наличии байтов; +- возможность расследовать расхождения публичного 1.13.5 hash/wall и W0 RSS по + исходным файлам. + +Исходные записи, по условию задачи #45, останутся лишь удобными входами для нового +минимального среза. Они не восстанавливают старые sidecars, prepared matrices, +слуховой plan, environment state или старые timing samples без повторного +вычисления. + +## Граница выводов + +Исторические строки выше описывают прежний каскад и прежние контрольные +фрагменты. Они отвечают на вопрос, какие локальные изменения уже проверены. +Новый эксперимент создает из сохраненных исходных записей собственные +фрагменты, ASR-sidecar, опорную разметку, baseline-диагностику и закрытое +хранилище результата. Числа `3 / 2 / 2`, `51 / 13 / 9` и прежняя purity +остаются характеристиками старых контролей. В новый gate переходят правила +точного совпадения с заново проверенным числом голосовых кластеров, +неизменности текста и безопасной памяти. + +C1 после удаления старого слухового материала остается исторически +неразрешенной. Для ее повторного открытия требуется свежая минимальная пара +W0/C1 и заново подготовленный материал для прослушивания. Карта и следующий +эксперимент эту работу не выполняют. + +## Класс архитектур и формула полной замены + +Текущий путь строит разметку говорящих каскадом `segmentation -> embeddings -> +clustering`. В sherpa-onnx 1.13.6 отдельный эмбеддинг вычисляется для каждой +пары локального говорящего и окна; это подтверждает +[upstream-реализация](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.6/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h). +Суммарное разложение выше показывает, что локальные изменения embeddings имеют +большой доступный бюджет, однако принятый 30%-й результат на Ryzen не получен. + +Для сравнения архитектур используется одна формула: + +```text +B = S + E + C + O +T_candidate = B - R + A +speedup = (R - A) / B +идеальный предел при A = 0: R / B +``` + +`B` - measured outer wall текущего W0; `S`, `E` и `C` - измеренные native +стадии; `O` - derived residual `outer wall - native engine total`; `R` - работа +старого пути, которую удаляет гипотеза; `A` - измеряемая работа новой +архитектуры. Для полной замены `R = B`, поэтому фактическое сокращение равно +`1 - A / B`. Upstream не публикует переносимый CPU RTF для выбранной модели; +числовой прогноз `A` отсутствует. Его определит свежий A/B. + +## Реестр архитектурных гипотез + +### A1. Streaming Sortformer v2 Q8_0 через NeMo-Speech.cpp - выбран + +**Механизм и затронутые стадии.** NEST/Fast-Conformer encoder и следующий за +ним Transformer выдают четыре sigmoid-потока активности. Arrival-Order Speaker +Cache связывает говорящих между streaming-чанками; порядок меток задается +первым появлением голоса. Модель возвращает `T x 4` вероятностей с шагом 80 мс +и заменяет текущие segmentation, embeddings, подсчет/кластеризацию и +финализацию. Архитектура описана в +[карточке модели NVIDIA](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2) +и [первичной статье](https://arxiv.org/abs/2507.18446). + +**Поведение quality gate.** Потери W1-W4 возникли после прореживания +10-секундных окон перед независимыми эмбеддингами. Sortformer оценивает +перекрывающиеся активности общей сетью и не использует эту сетку или порог AHC. +Ошибки могут перейти в границы 80-мс кадров, рождение канала и сохранение метки +в speaker cache, поэтому точное число кластеров и слуховая проверка остаются +обязательными. + +**Предел и риск.** Полная замена дает идеальный предел 100% вызова, а +фактическое сокращение `1 - A_sortformer / B`. Опубликованный RTF `0,002` +измерен на RTX 6000 Ada, batch 1, и не является CPU-прогнозом +([model card](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2#setting-up-streaming-configuration)). +Модель имеет четыре выходных канала говорящих; NVIDIA предупреждает о +деградации на пяти и более слышимых голосах, неанглийской речи, другом домене и +очень длинных записях +([technical limitations](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2#technical-limitations)). + +**Самая дешевая фальсификация.** На Ryzen выполнить один свежий W0 и одну +соседнюю пару W0/Sortformer на трех заново определенных пятиминутных +фрагментах. Остановить ветвь при несовпадении с новыми ожидаемыми числами +кластеров, нарушении текстовой инвариантности, OOM/swap/росте RSS без плато или +ускорении меньше 10%. Точный контракт находится в +[спецификации следующего эксперимента](../specs/2026-09-01-streaming-sortformer-cpu-experiment.md). + +**Повторное использование.** Переносятся рецепт Ryzen W0, правила gate, +каноническое сведение `Word` с `SpeakerInterval`, безопасные агрегаты и +сохраненные исходные записи. Sidecar, опорная разметка, фрагменты и закрытое +хранилище создаются заново. Prepared matrices, старые интервалы, патч и wheel +не используются. + +**Исполнимость.** NVIDIA публикует готовый Q8_0 GGUF, нативный Apache-2.0 +runtime с CPU backend и отдельным API диаризации +([NeMo-Speech.cpp](https://github.com/NVIDIA/NeMo-Speech.cpp), +[C ABI](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/include/nemo_speech/diar.h)). +Веса имеют CC BY 4.0. Runtime и модель закреплены в следующей спецификации. +Выбор разрешает только изолированный исследовательский стенд. + +### A2-A6. Фальсифицируемые альтернативы + +| ID | Механизм и отличие quality gate | Предел скорости | Главный риск | Самый дешевый тест | Что можно использовать повторно | Решение | +|---|---|---|---|---|---|---| +| `A2` LS-EEND | Causal encoder, online attractors и retention полностью заменяют `B`; нет окон Pyannote, отдельных voice embeddings и AHC. [Официальный код](https://github.com/Audio-WestlakeU/FS-EEND), [статья](https://doi.org/10.1109/TASLPRO.2025.3597446). | `1 - A_ls-eend / B`; CPU `A` не опубликована. | Публичные checkpoints привязаны к 8 кГц и доменам; готовой ONNX/GGUF CPU-поставки нет; возможен дрейф attractor. | До контрольного аудио проверить официальный checkpoint, лицензию, CPU device и однозначный 16-кГц/resampling contract; затем синтетический 60-секундный smoke. | Свежие входы, sidecar и общие gate; prepared embeddings неприменимы. | Отложить: A1 проверяет полную streaming-замену с готовым CPU runtime дешевле. | +| `A3` EEND-EDA | Self-attentive encoder и LSTM attractors выдают одновременные активности и собственный stop condition вместо AHC-N. [Статья](https://arxiv.org/abs/2005.09921), [официальная реализация](https://github.com/hitachi-speech/EEND). | `1 - A_eend-eda / B`; full-context attention может расти квадратично по длине. | Нет закрепленного универсального checkpoint и документированного CPU deployment; ошибка stop condition меняет N. | Сначала найти официальный checkpoint с лицензией, 16-кГц контрактом и CPU inference. До этого аудио не запускать. | Только свежие входы и общие gate. | Отложить до появления измеримого CPU artifact. | +| `A4` EEND-VC / DiariZen | Local EEND совместно строит активности и vectors, constrained clustering связывает чанки; старые пороговые отказы не обязаны повториться. [EEND-VC](https://arxiv.org/abs/2105.09040), [DiariZen](https://github.com/BUTSpeechFIT/DiariZen), [pruning](https://arxiv.org/abs/2506.18623). | Удаляются `S+E+C`, добавляются WavLM/Conformer и новый clustering; CPU `A` не опубликована. | Даже pruned-модель требует 3,8 GMAC/s; официальный рецепт ориентирован на CUDA/Python 3.10; веса CC BY-NC 4.0. | В изолированной среде проверить импорт CPU и 30-секундный публичный sample; закрыть при несовместимости или RTF хуже W0. | Свежие входы и общие gate. | Отклонить для короткого списка: CPU path и лицензия не подходят следующему дешевому срезу. | +| `A5` pyannote Community-1 | Powerset segmentation, WeSpeaker, VBx и exclusive diarization меняют границы и оценку N, сохраняя embedding cascade. [Исходник pipeline](https://github.com/pyannote/pyannote-audio/blob/main/src/pyannote/audio/pipelines/speaker_diarization.py), [model card](https://huggingface.co/pyannote/speaker-diarization-community-1), [powerset loss](https://arxiv.org/abs/2310.13025). | Гарантированно удаляется лишь старая clustering/часть сведения; ее measured ceiling близок к нулю. Batching остается неизмеренной реализационной гипотезой. | Тяжелый PyTorch runtime, предварительная авторизованная загрузка; dense segmentation и embedding cost сохраняются. | 60-секундный публичный CPU stage-profile с RSS; закрыть, если total не лучше W0 минимум на 10%. | Свежие слова, assignment-диагностика и категории прослушивания. | Отклонить: не атакует полную доминирующую работу каскада. | +| `A6` NeMo MSDD | MarbleNet VAD, multi-scale TitaNet embeddings, clustering и neural decoder меняют все стадии и восстанавливают overlap. [Статья](https://arxiv.org/abs/2203.15974), [официальный код](https://github.com/NVIDIA-NeMo/Speech/tree/main/examples/speaker_tasks/diarization). | Знак ускорения неизвестен: VAD сокращает тишину, несколько embedding scales и decoder добавляют работу. | Штатный pretrained MSDD ориентирован на телефонию; готового meeting GGUF/ONNX CPU path нет; начальная ошибка N переходит в decoder. | До аудио потребовать подходящий meeting checkpoint и CPU contract; затем публичный 30-секундный stage smoke. | Свежие входы и общие gate. | Отклонить: добавляет embedding work без готового целевого CPU runtime. | + +Реестр оставляет A2-A6 проверяемыми: для каждой ветви записан факт, который +может изменить решение. Второй кандидат в короткий список не добавлен, потому +что для него сначала пришлось бы создавать runtime или выбирать новые веса. + +## Решение и владение соседними задачами + +Следующее измерение проверяет ровно A1. Причины выбора: полная замена старого +вызова, готовые Q8_0 веса, закрепляемый нативный CPU runtime и standalone +разметка говорящих без изменения ASR. Контракт исполняет +[задача #46](https://git.dementev.space/ddmitry/local-transcriber/issues/46). +В задаче #45 модель и аудио не запускались. + +Параллельное планирование ASR и диаризации целиком принадлежит +[задаче #22](https://git.dementev.space/ddmitry/local-transcriber/issues/22). +Автоматическое включение принадлежит +[задаче #23](https://git.dementev.space/ddmitry/local-transcriber/issues/23). +Joint ASR+diarization и shared-ASR-encoder меняют путь распознавания и исключены +из этой карты. ASR-sidecar используется только как свежий quality input. + +Принятие A1 после эксперимента означает право подготовить отдельное +production-решение. Оно не меняет [ADR-007](../adr/007-word-level-speaker-diarization.md) +и не разрешает поставку NeMo-Speech.cpp в основном dependency graph. diff --git a/docs/specs/2026-09-01-streaming-sortformer-cpu-experiment.md b/docs/specs/2026-09-01-streaming-sortformer-cpu-experiment.md new file mode 100644 index 0000000..52d3eca --- /dev/null +++ b/docs/specs/2026-09-01-streaming-sortformer-cpu-experiment.md @@ -0,0 +1,472 @@ +# Эксперимент Streaming Sortformer v2 Q8_0 на Ryzen CPU + +**Дата:** 2026-09-01 + +**Статус:** готовый контракт следующего эксперимента, исполняемый в +[задаче #46](https://git.dementev.space/ddmitry/local-transcriber/issues/46). +Запуск модели и аудио начинается после подготовки свежей закрытой основы. + +## Цель и исходы + +Эксперимент отвечает на один вопрос: сокращает ли Streaming Sortformer v2 +Q8_0 через NeMo-Speech.cpp время построения разметки говорящих на целевом Ryzen +минимум на 30% относительно свежего последовательного W0 при приемлемом +качестве и памяти. + +Допустимы три исхода: + +- `go`: медиана пяти финальных парных ускорений не ниже 30%, автоматические и + слуховые gates пройдены; +- `stop`: нарушен обязательный gate, первичное ускорение ниже 10%, финальная + медиана ниже 30% или слуховая проверка подтвердила регрессию; +- `needs-user`: отсутствует подтверждение свежей опоры или требуемое решение + по прослушиванию. Это состояние ожидания, его нельзя публиковать как результат + кандидата. + +`go` разрешает подготовку отдельного production-решения. Экспериментальный +runtime остается изолированным, а production-профиль, CLI, config и модель по +умолчанию сохраняются. + +## Сначала прочитать + +1. Прочитать [карту архитектур](../research/2026-09-01-end-to-end-cpu-diarization-map.md), + особенно разделы "Граница выводов", A1 и формулу полной замены. + Критерий завершения: агент может объяснить, почему старые значения + `3 / 2 / 2`, unmatched words и purity не являются порогами нового опыта. +2. Прочитать [ADR-007](../adr/007-word-level-speaker-diarization.md) и + [старую спецификацию](2026-08-30-cpu-diarization-experiment.md). Критерий + завершения: сохранены каноническое сведение слов, правила 5%/10%/30% и + memory gate. +3. Прочитать `src/local_transcriber/benchmark_experiment.py` и + `tests/test_benchmark_experiment.py` перед реализацией. Критерий завершения: + новый стенд использует отдельный модуль и schema, не принимает старый + manifest или закрытые хранилища задач #41/#42. + +## Границы + +В scope входят свежая закрытая основа, точный W0/Sortformer A/B на Ryzen, +проверка трех полных записей после прохождения 30%-го порога на фрагментах и +слуховая приемка. + +За пределами scope остаются: + +- replay W/M/Q/E/C и восстановление старых patch/wheel; +- C1; ее повторное открытие требует отдельной свежей W0/C1 пары и нового + слухового материала; +- Intel и другие CPU; +- production integration, dependency lock, упаковка runtime и изменение ADR; +- параллельное планирование ASR/диаризации - это + [задача #22](https://git.dementev.space/ddmitry/local-transcriber/issues/22); +- автоматическое включение - это + [задача #23](https://git.dementev.space/ddmitry/local-transcriber/issues/23); +- joint ASR+diarization и shared-ASR-encoder. ASR создает только quality + sidecar до измерений. + +## Закрепленные реализации + +### W0 + +W0 использует действующий последовательный путь на восьми физических ядрах: + +- `sherpa-onnx/core 1.13.6`; +- Pyannote Segmentation 3.0 FP32, SHA-256 + `220ad67ca923bef2fa91f2390c786097bf305bceb5e261d4af67b38e938e1079`; +- WeSpeaker ResNet34 LM FP32, SHA-256 + `e9848563da86f263117134dfd7ad63c92355b37de492b55e325400c9d9c39012`; +- `window_shift_ratio=0.1`; +- `FastClustering(auto, 0.89)`; +- `provider=cpu` для segmentation и embeddings; +- `debug=True` для обеих model configs: stage times читаются из штатного + debug-журнала и входят в измерительный рецепт; +- `min_duration_on=0.3`, `min_duration_off=0.5`; +- один worker, одна session, `intra_op_threads=8`, `inter_op_threads=1`, batch 1; +- штатный непатченный `diarizer.process(samples)`. + +Старые experimental patch/wheel не загружаются и не восстанавливаются. До +первого запуска стенд проверяет версии, хеши и все effective values рецепта. +Критерий завершения: `--validate-only` показывает рецепт `W0-FRESH` и +останавливается при любом несовпадении. + +### Sortformer + +Runtime: + +- репозиторий [NVIDIA/NeMo-Speech.cpp](https://github.com/NVIDIA/NeMo-Speech.cpp); +- commit `4f9676226f667d14608487df744f375db87127f8`; +- license Apache-2.0; +- source build штатным CPU-only preset: + `git submodule update --init ggml`, затем `scripts/configure.sh cpu-diar` и + `cmake --build --preset cpu-diar` + ([инструкция build](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/docs/build.md), + [точный preset](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/CMakePresets.json)); +- preset использует Release/Ninja, включает только diarization и отключает + patched GGML; runtime запускается с `gpu=-1`, одним процессом и одним stream; +- CPU graph использует закрепленные в этом commit четыре внутренних потока; + runtime knob для их изменения отсутствует + ([вызов scheduler](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/src/runtime/ggml/session.cpp#L903)); +- submodule commits, compiler, CMake options и SHA-256 готового executable и + shared library сохраняются в новом manifest. + +Модель: + +- repo `nvidia/diar_streaming_sortformer_4spk-v2`; +- revision `5240a64075176943f677d30fa2171c780229f341`; +- файл `diar_streaming_sortformer_4spk-v2.q8_0.gguf`; +- размер `147075776` байт; +- SHA-256 `0679cfeb1ce356d0dea9470b31274f4bfc7eb927497d82005483770666da998a`; +- license CC BY 4.0. + +Эти revision, размер и хеш также закреплены в +[официальном model index выбранного runtime](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/models/index.json#L75-L88). +После подготовки байтов стенд работает без сети. + +Кандидат вызывается через +[standalone C ABI](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/include/nemo_speech/diar.h), +чтобы сохранить effective config и покадровую диагностику. Stateful streaming +получает точную геометрию NVIDIA в 80-мс кадрах: + +| Поле | Значение | +|---|---:| +| `chunk_frames` | `340` | +| `right_context_frames` | `40` | +| `left_context_frames` | `0` | +| `fifo_frames` | `40` | +| `spkcache_frames` | `188` | +| `update_period_frames` | `300` | + +Сумма chunk и right context дает input buffer latency 30,4 с. Опубликованный +RTF этой конфигурации измерен на RTX 6000 Ada и в CPU-гипотезу не переносится +([model card](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2#setting-up-streaming-configuration)). +Full-attention `--offline` не используется. + +Post-processing передается явно при каждом запросе: + +| Поле | Значение | +|---|---:| +| `onset` | `0.641` | +| `offset` | `0.561` | +| `pad_onset_sec` | `0.229` | +| `pad_offset_sec` | `0.079` | +| `min_duration_sec` | `0.511` | +| `min_gap_sec` | `0.296` | + +Это callhome-tuned конфигурация закрепленного runtime. Она остается quality +риском для русских рабочих встреч и не настраивается по трем контрольным +фрагментам. Любое изменение геометрии, post-processing, quantization или +runtime commit создает другую гипотезу и требует новой спецификации. + +Критерий завершения подготовки кандидата: сборка выполняет синтетический +ABI-тест без аудио, `nemo_speech_diar_num_speakers()` возвращает 4, +`nemo_speech_diar_seconds_per_frame()` возвращает 0,08, а manifest содержит все +закрепленные значения и хеши. Синтетический тест не заменяет smoke-проверку с +аудио. + +## Свежая закрытая основа + +### Шаг 1. Выбрать источники и фрагменты + +Владелец выбирает три сохраненные исходные записи и по одной 300-секундной +области в каждой. Набор должен включать русскую рабочую речь, смену говорящего, +короткий ответ и перекрытие хотя бы где-то в совокупности. В каждой области и +каждой полной записи должно быть от двух до четырех слышимых голосов. Запись с +пятью голосами закрывает кандидат до измерения: модель выдает не более четырех +выходных голосовых кластеров +([technical limitations](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2#technical-limitations)). + +В закрытом manifest сохраняются source hash, точные границы и recipe +mono/16 kHz float32. Публичные документы получают только число фрагментов, +длительность и агрегаты. + +Критерий завершения: три source hash уникальны, три WAV имеют ровно 300 секунд +после единственного закрепленного преобразования, а ни один путь, ID или +интервал не попадает в stdout режима `--validate-only`. + +### Шаг 2. Создать ASR-sidecar и опорную разметку + +Действующий ASR выполняется один раз для каждой полной записи до измерений. +Sidecar хранит канонические слова с временной привязкой и нормализованный текст; +fragment-sidecar получается детерминированным срезом той же версии. Затем +человек прослушивает каждый фрагмент целиком, фиксирует ожидаемое число +голосовых кластеров для фрагментов и полных записей и создает опорную разметку, +достаточную для mapped purity и выбора мест прослушивания. Опорная разметка +остается приближенной и не объявляется истиной границ. + +Критерий завершения: для каждого фрагмента и каждой полной записи есть +подтвержденное человеком `expected_cluster_count` в диапазоне `2..4`, один +неизменяемый sidecar и SHA-256; для фрагментов также есть опорная разметка. +Числа старых контролей не копируются. + +### Шаг 3. Пройти пользовательский gate + +До первого модельного запуска владелец подтверждает: + +- право использовать выбранные записи в закрытом локальном опыте; +- пригодность трех областей и полных записей, а также + `expected_cluster_count` каждого фрагмента и каждой полной записи; +- доступность исходных полных записей для этапа после 30%-го порога на + фрагментах; +- готовность выполнить безусловный финальный слуховой gate, если медиана + достигнет 30%. Diagnostics только добавляют обязательные места прослушивания. + +Критерий завершения: в закрытом manifest есть timestamped decision `approved`. +Без него исход `needs-user`; стенд не загружает модели. + +## Измеряемая величина и A/B + +Время W0 - wall вокруг одного `diarizer.process(samples)`. Время кандидата - +wall вокруг `stream_open -> push all prepared samples -> finish -> segment +post-processing`. Для обеих ветвей модель уже загружена, WAV уже декодирован в +один и тот же mono/16 kHz float32 buffer. ASR, скачивание, build, model load, +декодирование, чтение sidecar, запись artifacts и расчет метрик находятся вне +таймера. + +Полная замена оценивается только измерением: + +```text +B = measured W0 wall +A = measured Sortformer wall +speedup_percent = 100 * (B - A) / B +``` + +Восьмиклеточное историческое Ryzen-разложение не подставляется вместо свежего +`B`. CPU prediction для `A` отсутствует. + +Каждая ячейка запускается новым дочерним процессом на одной машине, от сети, в +стабильном power mode. Process affinity разрешает те же восемь физических ядер +обеим ветвям. W0 получает `intra_op_threads=8`; закрепленный candidate runtime +исполняет CPU graph четырьмя внутренними потоками. Это часть сравниваемого +runtime recipe. Изменение hardcoded числа потоков образует новую гипотезу. +Фиксируются logical/physical cores, CPUID/ISA, RAM, ОС, compiler/runtime +versions, affinity, effective threads, power mode, governor, turbo, throttling +и swap. Прогон со сменой режима или throttling не учитывается и повторяется +один раз после стабилизации. + +### Шаг 4. Принять свежий W0 + +Один `W0-FRESH` обрабатывает три фрагмента последовательно. Для каждого +сохраняются raw wall/CPU/RSS и stage times; публично выводится сумма. W0 должен +дать ровно подтвержденные `expected_cluster_count`, сохранить sidecar-текст и +пройти memory gate. + +Критерий завершения: W0 прошел все обязательные gates и его закрытый result +имеет SHA-256. При провале итог `stop`: база сравнения непригодна. + +### Шаг 5. Выполнить первичный A/B + +Сразу после принятого W0 запускается одна ячейка кандидата на тех же buffers и в +том же порядке фрагментов. Primary order закреплен как `W0 -> candidate`; пять +дорогих циклов ниже устраняют постоянный порядок чередованием. Один primary +cycle равен сумме трех wall times каждой ветви. + +Критерий завершения: обе ячейки имеют raw result, одинаковый список input +hashes и один pair ID; рассчитаны ускорение, CPU и RSS delta. + +Решение после primary: + +- `abs(speedup) <= 5%` - результат фиксируется как равенство по скорости; +- `speedup < 10%` - `stop`: причина `equal-speed` для равенства по скорости, + иначе `below-continuation`; +- `speedup >= 10%` при всех gates - перейти к пяти финальным циклам; +- любой обязательный quality/memory/Sortformer gate - немедленный `stop`. + +### Шаг 6. Выполнить пять финальных циклов + +Каждый цикл содержит свежую ячейку W0 и свежую ячейку кандидата на всех трех +фрагментах. Primary pair в пять финальных циклов не входит. Порядок закреплен +заранее и чередуется так: + +1. final 1: `candidate -> W0`; +2. final 2: `W0 -> candidate`; +3. final 3: `candidate -> W0`; +4. final 4: `W0 -> candidate`; +5. final 5: `candidate -> W0`. + +Вместе с primary `W0 -> candidate` это исключает выбор порядка по уже видимому +результату. Дополнительные варианты, модели и настройки не добавляются. + +Критерий завершения: получены пять парных ускорений, их медиана и MAD; все +десять ячеек прошли gates. Медиана не ниже 30% открывает проверку полных записей. +Медиана ниже 30% дает `stop`. Дополнительные повторы не назначаются. + +### Шаг 7. Проверить полные записи и пройти слуховой gate + +После медианы не ниже 30% W0 и candidate однократно обрабатывают три полные +исходные записи. Скорость полных прогонов остается диагностикой и не заменяет +пятипарную медиану. + +Владелец получает blinded A/B материал по пяти категориям: + +- короткие ответы и перебивки; +- смены говорящего; +- перекрывающаяся речь; +- устойчивость соответствия голоса кластеру; +- слова без говорящего и малые остаточные кластеры. + +Материал включает все места, отмеченные сравнительными diagnostics, и +фиксированную случайную выборку совпадающих мест. Порядок A/B и имя кандидата +раскрываются после вердикта. По каждой категории владелец записывает `pass` или +`fail` и короткую причину в закрытом хранилище; публично остается только +вердикт. + +Критерий завершения: три пары на полных записях прошли автоматические gates, +все пять слуховых категорий имеют решение. Любой подтвержденный `fail` дает +`stop`; полный `pass` завершает эксперимент как `go`. + +## Gates и диагностика + +### Обязательное качество и корректность + +Каждая ячейка W0 и кандидата обязана выполнить все условия: + +- число голосовых кластеров каждого обрабатываемого контрольного объекта - + фрагмента или полной записи - точно равно свежему, подтвержденному человеком + `expected_cluster_count`; +- нормализованный текст и порядок слов побайтно совпадают с одним sidecar; +- нет потерянных, продублированных или переставленных слов; +- интервалы конечны, имеют положительную длительность, лежат в пределах audio + duration с учетом явно заданного padding и отсортированы по началу; +- каноническая нумерация меток детерминирована на повторном расчете метрик; +- все inputs, параметры и output artifacts имеют SHA-256. + +Purity и число слов без говорящего сравниваются с новым `W0-FRESH`. Ухудшение +не дает автоматического `fail`, но обязательно добавляет все затронутые места в +слуховой gate. Старые абсолютные значения не участвуют. + +Открытый диагностикой слуховой gate не прерывает шаги 5-6. Стенд накапливает +затронутые места и добавляет их в материал шага 7. Состояние `needs-user` из-за +прослушивания возможно только после достижения 30%-го порога и подготовки +полного blinded-материала. + +### Sortformer-specific gate + +Для каждой ячейки кандидата сохраняются и проверяются: + +- `num_speakers=4`, `seconds_per_frame=0.08` и точная geometry/post-processing + из этой спецификации; +- `frame_count`, `frame_probs_start` и размер доступной матрицы `T x 4`; +- каждое сохраненное probability конечно и принадлежит `[0, 1]`; +- каждая итоговая speaker label принадлежит `1..4`; +- число и суммарная длительность интервалов по каждому каналу; +- число коротких интервалов, overlap ratio, unmatched words, малые/остаточные + кластеры и mapped purity относительно свежей опорной разметки; +- wall отдельно для model inference и segment post-processing. + +Любой неверный shape/value/label или эффективная конфигурация, отличная от +закрепленной, дает `stop`. Появление лишнего/пропажа ожидаемого кластера дает +`stop`. Изменение channel stability, short-turn, overlap, unmatched-word или +purity diagnostics открывает слуховую проверку; числовой порог для этих proxy +не вводится. + +Жесткий предел четырех выходных голосовых кластеров, риск записей с пятью и +более слышимыми голосами и риск non-English/out-of-domain сохраняются в +публичном решении даже после `go`; опыт не доказывает применимость к другим +встречам. + +### Память + +Peak RSS измеряется process-wide для каждой ячейки. Также сохраняются swap до и +после, page faults и RSS после каждого фрагмента. + +Memory gate дает `stop` при OOM, активной подкачке во время ячейки, RSS без плато +между повторами или линейном росте RSS с числом обработанных фрагментов. Peak +RSS выше 2 ГиБ на машине с 16 ГиБ требует отдельного разбора владельца и +состояния `needs-user`; одно превышение не является автоматическим `fail`. + +## Артефакты и приватность + +Checkout и дерево сборки NeMo-Speech.cpp, скачанные веса и новое закрытое +хранилище размещаются вне рабочей копии local-transcriber. Обычная `.scratch` +внутри репозитория не подходит: `.gitignore` покрывает там только +`.scratch/handoffs/`. Ни один runtime/model/build artifact или закрытый вход не +попадает в Git. + +Новое закрытое хранилище содержит: + +- versioned manifest со schema, clean local-transcriber source commit, + source/model/runtime/build hashes и расписанием; +- исходные ссылки, подготовленные WAV и recipe преобразования; +- ASR-sidecar и опорную разметку; +- raw per-fragment results, intervals, word assignments и probability + diagnostics; +- machine/power/runtime guards и stderr/stdout logs; +- материал и решения слуховой проверки; +- байты runtime/model либо проверяемый download recipe с revision, size и hash. + +Хранилище не ссылается на закрытые хранилища задач #41/#42 и остается +достаточным для разбора собственного результата. Удаление старых хранилищ не +блокирует ни один шаг. + +Публичный отчет содержит `clean local-transcriber source commit`, точные +рецепты, хеши, агрегаты, вердикты gates и итоговое решение. В Git, комментарии +Gitea, задания на ревью и вывод `--validate-only` не попадают пути, +`source/recording IDs`, текст, временные интервалы, сырые строки, содержимое +manifest, логи, имена участников или аудио. + +При нарушении приватности стенд останавливается до публикации. Уже +сформированный публичный артефакт удаляется из рабочей ветки и создается заново +из безопасных агрегатов. + +## Реализация стенда и тесты + +Новый стенд реализуется отдельным модулем +`src/local_transcriber/streaming_sortformer_experiment.py`; существующий +`benchmark_experiment.py` остается источником общих проверенных механизмов. +Стенд получает отдельную schema и только две recipe: `W0-FRESH` и +`SORTFORMER-V2-Q8`. Manifest validation выполняется до загрузки моделей. +`--validate-only` проверяет: + +- три уникальных source hashes, 300-секундные фрагменты и fresh-basis marker; +- human approval и `expected_cluster_count` каждого фрагмента и каждой полной + записи в диапазоне `2..4`; +- W0 model/runtime hashes и effective values: CPU provider, `debug=True`, + `min_duration_on=0.3`, `min_duration_off=0.5`, threads и clustering; +- clean local-transcriber worktree и source commit из manifest; +- Sortformer revision, size, SHA-256, runtime commit, submodules, build hashes, + CPU backend, geometry и post-processing; +- одинаковые input/sidecar hashes обеих ветвей; +- заранее созданные primary/final schedules; +- отсутствие корней старых хранилищ и legacy ID ячеек. + +Минимальные детерминированные тесты выполняются без модели и аудио: + +```bash +uv run pytest -q \ + tests/test_streaming_sortformer_experiment.py \ + tests/test_benchmark_experiment.py \ + tests/test_speaker_diarizer.py \ + tests/test_diarization.py +``` + +Новый модуль тестов покрывает отклонение `schema`, несовпадение пинов и хешей, +требование свежей опоры, обезличенный вывод `--validate-only`, правило +`expected_cluster_count`, точное расписание A/B, границы 5%/10%/30%, gates +качества, памяти и Sortformer, переход к прослушиванию, безопасную сериализацию +и остановку при первом отказе. Стенд C ABI имеет отдельный синтетический тест +размеров структур, выбора CPU и эффективной конфигурации; модель в модульных +тестах не загружается. + +Критерий завершения реализации стенда: все детерминированные тесты проходят, +`--validate-only` проходит на закрытом manifest и публичный вывод проходит +privacy scan. Реализация стенда фиксируется чистым коммитом исходников, +совпадающим с manifest. Только после этого разрешен первый запуск модели W0. + +## Критерии завершения эксперимента + +- [ ] Свежая опора подтверждена владельцем и не наследует старые контрольные + значения. +- [ ] Runtime, модель, сборка и оба рецепта закреплены и проверены до моделей. +- [ ] Clean local-transcriber source commit сохранен в закрытом хранилище и + публичном отчете, а `--validate-only` проверяет его до моделей. +- [ ] Свежий W0 принят либо итог `stop` объясняет провал базы сравнения. +- [ ] Первичный A/B завершен; решение 10% применено без дополнительных ветвей. +- [ ] При продолжении завершены ровно пять финальных циклов, медиана и MAD. +- [ ] Каждая выполненная ячейка имеет вердикты качества, текста, памяти и + Sortformer. +- [ ] При медиане 30% выполнены три пары на полных записях и пять слуховых + решений. +- [ ] Все остановленные шаги имеют первую точную причину и хеш результата. +- [ ] Закрытое хранилище самодостаточно; публичный отчет содержит только безопасные + агрегаты и SHA-256. +- [ ] Итог равен `go` или `stop`; `needs-user` остается открытым до решения. +- [ ] Ни один результат автоматически не меняет production-зависимости, + профиль, CLI, config или модель по умолчанию.