Files
local-transcriber/docs/specs/2026-09-01-streaming-sortformer-cpu-experiment.md
T
Dmitry Dementiev 15f88b1de8 docs(diarization): отложено удаление архивов
- Зачем:
  - старые результаты решено сохранить до завершения и приемки задачи #46.
- Что:
  - архивы задач #41/#42 закреплены как временные и доступные только для чтения;
  - стенду #46 запрещено читать архивы или использовать их как основу A/B;
  - удаление перенесено на момент после приемки нового отчета.
- Проверка:
  - выполнены git diff --check и проверка формулировки в задаче #46.
2026-09-01 16:36:11 +03:00

479 lines
33 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.
# Эксперимент 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 получается детерминированным срезом той же версии. Затем
человек прослушивает каждый фрагмент целиком, фиксирует ожидаемое число
голосовых кластеров для фрагментов и полных записей и создает опорную разметку,
достаточную для 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 и остается
достаточным для разбора собственного результата. После приемки отчета задачи
#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.
## Критерии завершения эксперимента
- [ ] Свежая опора подтверждена владельцем и не наследует старые контрольные
значения.
- [ ] 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 или модель по умолчанию.