# Эксперимент 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` и останавливается при любом несовпадении. Закрытые хранилища задач #41/#42 временно сохраняются до завершения и приемки этого эксперимента как архив только для чтения. Стенд не получает их пути, не читает старые manifest и результаты и отклоняет их ID при валидации. Архив не является входом, базой сравнения или условием получения нового результата. ### 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 получается детерминированным срезом той же версии. Затем человек прослушивает каждый фрагмент целиком и фиксирует ожидаемое число голосовых кластеров для фрагментов и полных записей. Опорная разметка фрагмента необязательна: `reference_path` и `reference_sha256` либо заполнены вместе, либо оба равны `null`. При наличии разметки стенд считает mapped purity и выбирает места прослушивания; сама разметка остается приближенной и не объявляется истиной границ. Критерий завершения: для каждого фрагмента и каждой полной записи есть подтвержденное человеком `expected_cluster_count` в диапазоне `2..4`, один неизменяемый sidecar и SHA-256. Отсутствие опорной разметки не блокирует измерение и не ослабляет остальные gates. Для канонического Ryzen-прогона используются подтвержденные владельцем числа `3 / 2 / 2`; их привязывают к источникам по хешам, а не по порядку нейтральных имен. Повторное прослушивание для подсчета голосов не требуется. ### Шаг 3. Пройти пользовательский gate До первого модельного запуска владелец подтверждает: - право использовать выбранные записи в закрытом локальном опыте; - пригодность трех областей и полных записей, а также `expected_cluster_count` каждого фрагмента и каждой полной записи; - доступность исходных полных записей для этапа после 30%-го порога на фрагментах; - готовность выполнить безусловный финальный слуховой gate, если медиана достигнет 30%. Diagnostics только добавляют обязательные места прослушивания. Критерий завершения: в закрытом manifest есть timestamped decision `approved`. Без него исход `needs-user`; стенд не загружает модели. В каноническом Ryzen-прогоне решением `approved` служит датированный handoff владельца в комментарии к задаче #46. Нового подтверждения до достижения 30%-го порога не требуется. ## Измеряемая величина и 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` внутри репозитория целиком игнорируется, но остается локальной частью конкретного checkout и не подходит для закрытого переносимого хранилища. Ни один 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 и остается достаточным для разбора собственного результата. После приемки отчета задачи #46 старые архивы можно удалить; их удаление не блокирует ни один шаг и не лишает новый результат доказательной основы. Публичный отчет содержит `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. ## Критерии завершения эксперимента - [ ] Свежая опора собрана в новом закрытом хранилище; подтвержденные входы, доступная разметка и числа голосов связаны по хешам, а старые manifest, расписание и результаты не наследуются. - [ ] 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 или модель по умолчанию.