experiment(diarization): сопоставлены W0 и Sortformer на Ryzen #53
No files matched your search
+1
-1
@@ -7,4 +7,4 @@ dist/
|
||||
.codex
|
||||
.qwen/
|
||||
tmp
|
||||
.scratch/handoffs/
|
||||
/.scratch/
|
||||
@@ -5,6 +5,11 @@
|
||||
Не часть пакета: они опираются на `sherpa-onnx`, которого нет в зависимостях
|
||||
проекта, и живут в `.scratch/`, а не в `src/`.
|
||||
|
||||
Код этого каталога временно закреплен в Git: его используют несколько задач на
|
||||
нескольких компьютерах, пока идет исследование диаризации. Общее правило
|
||||
`/.scratch/` относится к новым локальным материалам; уже отслеживаемые файлы
|
||||
этого стенда продолжают версионироваться.
|
||||
|
||||
Результаты первого прогона описаны в
|
||||
[разведочном замере](../../docs/benchmarks/2026-08-12-diarization-feasibility.md).
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Streaming Sortformer: сравнение на Ryzen
|
||||
|
||||
**Дата:** 2026-09-04
|
||||
|
||||
Streaming Sortformer обработал три 300-секундных фрагмента за `67,433` с
|
||||
против `103,242` с у свежего W0. Ускорение одной пары составило `34,68%`.
|
||||
Кандидат правильно определил число голосов на всех трех фрагментах; W0 на
|
||||
одном из них выделил два кластера вместо трех.
|
||||
|
||||
У результата есть два заметных минуса. Sortformer оставил без говорящего
|
||||
`92` слова против `73` у W0 и потребовал около `551` МиБ памяти против `325`
|
||||
МиБ. Во время обеих ячеек была небольшая активность swap, поэтому одна пара
|
||||
показывает направление, но не устойчивость ускорения.
|
||||
|
||||
## Сравнение
|
||||
|
||||
| Показатель | W0 | Sortformer |
|
||||
| --- | ---: | ---: |
|
||||
| Суммарный wall, с | `103,242` | `67,433` |
|
||||
| Ускорение | - | `34,68%` |
|
||||
| Ожидалось кластеров | `2 / 2 / 3` | `2 / 2 / 3` |
|
||||
| Получено кластеров | `2 / 2 / 2` | `2 / 2 / 3` |
|
||||
| Ошибок по числу кластеров | `1 из 3` | `0 из 3` |
|
||||
| Mapped purity на двух размеченных фрагментах | `0,7655 / 0,8915` | `0,8392 / 0,9176` |
|
||||
| Слов без говорящего | `73` | `92` |
|
||||
| Пиковый RSS, МиБ | `324,57` | `551,07` |
|
||||
| CPU time, с | `822,67` | `238,70` |
|
||||
| Прочитано из swap, КиБ | `492` | `224` |
|
||||
| Записано в swap, КиБ | `148` | `160` |
|
||||
|
||||
Обе модели получили одинаковые заранее подготовленные mono/16 kHz float32
|
||||
буферы и те же ASR-sidecar. W0 работал на восьми физических ядрах, Sortformer -
|
||||
с четырьмя закрепленными внутренними потоками. Загрузка моделей, декодирование
|
||||
аудио, чтение sidecar и расчет метрик в wall не входили.
|
||||
|
||||
Sortformer прошел автоматические проверки формы вероятностей, диапазона
|
||||
значений, меток говорящих, текста, порядка слов и интервалов на всех трех
|
||||
фрагментах. Его mapped purity выше W0 на обоих фрагментах с опорной разметкой.
|
||||
При этом рост числа слов без говорящего требует слуховой проверки: по одной
|
||||
автоматике нельзя заключить, что качество кандидата в целом лучше.
|
||||
|
||||
## Как читать результат
|
||||
|
||||
По исходному расписанию задача завершилась как `stop`, когда W0 не прошел
|
||||
проверку числа кластеров. После этого была выполнена одна диагностическая
|
||||
ячейка Sortformer на тех же входах, чтобы ответить на практический вопрос о
|
||||
кандидате. Пять повторных пар, полные записи и слуховая проверка не выполнялись.
|
||||
|
||||
По этой паре Sortformer выглядит перспективно: ускорение оказалось выше
|
||||
целевого порога `30%`, модель правильно выделила три голоса на проблемном
|
||||
фрагменте и показала лучшую mapped purity. Для решения о внедрении нужны
|
||||
повторные парные замеры без активности swap и слуховая проверка мест, где
|
||||
слова остались без говорящего. Текущий результат уже достаточен, чтобы не
|
||||
отбрасывать кандидата и продолжить исследование отдельной задачей.
|
||||
|
||||
## Рецепты и техническая привязка
|
||||
|
||||
W0 использовал `sherpa-onnx/core 1.13.6`, Pyannote Segmentation 3.0 FP32,
|
||||
WeSpeaker ResNet34 LM FP32, CPU provider, `window_shift_ratio=0.1`,
|
||||
`FastClustering(auto, 0.89)`, один процесс и одну сессию,
|
||||
`intra_op_threads=8`, `inter_op_threads=1`, `min_duration_on=0.3` и
|
||||
`min_duration_off=0.5`.
|
||||
|
||||
Кандидат использовал NeMo-Speech.cpp commit
|
||||
`4f9676226f667d14608487df744f375db87127f8`, модель Streaming Sortformer v2
|
||||
Q8_0 с SHA-256
|
||||
`0679cfeb1ce356d0dea9470b31274f4bfc7eb927497d82005483770666da998a`, CPU
|
||||
backend, один процесс и один stream. Geometry и post-processing соответствовали
|
||||
[спецификации](../specs/2026-09-01-streaming-sortformer-cpu-experiment.md).
|
||||
|
||||
Коммит исходного кода, выполнившего обе ячейки:
|
||||
`4c4a8be07b3653cede35b0435e6c9ef6cb3fb87d`.
|
||||
|
||||
Закрытая основа и результаты сохраняются для следующих исследований. Их
|
||||
повторное использование должно быть явно описано в новой задаче.
|
||||
@@ -151,9 +151,10 @@
|
||||
|
||||
За пределами Git остаются manifest, записи, WAV-кеш, ASR-sidecar, опорные
|
||||
транскрипты, сырые интервалы, результаты, worker requests, журналы и материалы
|
||||
слуховой проверки. Рабочий каталог стенда нельзя размещать в обычной
|
||||
`.scratch`: репозиторий игнорирует там только служебный каталог
|
||||
`.scratch/handoffs/`, не связанный с переносом эксперимента между ноутбуками.
|
||||
слуховой проверки. Рабочий каталог стенда нельзя размещать в `.scratch`: этот
|
||||
каталог локален и целиком игнорируется, поэтому эксперимент из него не
|
||||
переносится между ноутбуками. Временно закрепленные диагностические скрипты уже
|
||||
отслеживаются Git и не меняют этого правила для новых материалов.
|
||||
|
||||
Публичный отчет содержит хеши, агрегаты, конфигурацию и выводы. В него не входят
|
||||
локальные пути, распознанный текст, временные интервалы, имена участников и
|
||||
|
||||
@@ -186,20 +186,25 @@ mono/16 kHz float32. Публичные документы получают то
|
||||
после единственного закрепленного преобразования, а ни один путь, ID или
|
||||
интервал не попадает в stdout режима `--validate-only`.
|
||||
|
||||
### Шаг 2. Создать ASR-sidecar и опорную разметку
|
||||
### Шаг 2. Подготовить ASR-sidecar и доступную опорную разметку
|
||||
|
||||
Действующий ASR выполняется один раз для каждой полной записи до измерений.
|
||||
Sidecar хранит канонические слова с временной привязкой и нормализованный текст;
|
||||
fragment-sidecar получается детерминированным срезом той же версии. Затем
|
||||
человек прослушивает каждый фрагмент целиком, фиксирует ожидаемое число
|
||||
голосовых кластеров для фрагментов и полных записей и создает опорную разметку,
|
||||
достаточную для mapped purity и выбора мест прослушивания. Опорная разметка
|
||||
остается приближенной и не объявляется истиной границ.
|
||||
человек прослушивает каждый фрагмент целиком и фиксирует ожидаемое число
|
||||
голосовых кластеров для фрагментов и полных записей. Опорная разметка фрагмента
|
||||
необязательна: `reference_path` и `reference_sha256` либо заполнены вместе, либо
|
||||
оба равны `null`. При наличии разметки стенд считает mapped purity и выбирает
|
||||
места прослушивания; сама разметка остается приближенной и не объявляется
|
||||
истиной границ.
|
||||
|
||||
Критерий завершения: для каждого фрагмента и каждой полной записи есть
|
||||
подтвержденное человеком `expected_cluster_count` в диапазоне `2..4`, один
|
||||
неизменяемый sidecar и SHA-256; для фрагментов также есть опорная разметка.
|
||||
Числа старых контролей не копируются.
|
||||
неизменяемый sidecar и SHA-256. Отсутствие опорной разметки не блокирует
|
||||
измерение и не ослабляет остальные gates. Для канонического Ryzen-прогона
|
||||
используются подтвержденные владельцем числа `3 / 2 / 2`; их привязывают к
|
||||
источникам по хешам, а не по порядку нейтральных имен. Повторное прослушивание
|
||||
для подсчета голосов не требуется.
|
||||
|
||||
### Шаг 3. Пройти пользовательский gate
|
||||
|
||||
@@ -216,6 +221,10 @@ fragment-sidecar получается детерминированным сре
|
||||
Критерий завершения: в закрытом manifest есть timestamped decision `approved`.
|
||||
Без него исход `needs-user`; стенд не загружает модели.
|
||||
|
||||
В каноническом Ryzen-прогоне решением `approved` служит датированный handoff
|
||||
владельца в комментарии к задаче #46. Нового подтверждения до достижения
|
||||
30%-го порога не требуется.
|
||||
|
||||
## Измеряемая величина и A/B
|
||||
|
||||
Время W0 - wall вокруг одного `diarizer.process(samples)`. Время кандидата -
|
||||
@@ -333,9 +342,10 @@ hashes и один pair ID; рассчитаны ускорение, CPU и RSS
|
||||
- каноническая нумерация меток детерминирована на повторном расчете метрик;
|
||||
- все inputs, параметры и output artifacts имеют SHA-256.
|
||||
|
||||
Purity и число слов без говорящего сравниваются с новым `W0-FRESH`. Ухудшение
|
||||
не дает автоматического `fail`, но обязательно добавляет все затронутые места в
|
||||
слуховой gate. Старые абсолютные значения не участвуют.
|
||||
Purity для фрагментов с опорной разметкой и число слов без говорящего
|
||||
сравниваются с новым `W0-FRESH`. Ухудшение не дает автоматического `fail`, но
|
||||
обязательно добавляет все затронутые места в слуховой gate. Старые абсолютные
|
||||
значения не участвуют.
|
||||
|
||||
Открытый диагностикой слуховой gate не прерывает шаги 5-6. Стенд накапливает
|
||||
затронутые места и добавляет их в материал шага 7. Состояние `needs-user` из-за
|
||||
@@ -353,7 +363,7 @@ Purity и число слов без говорящего сравниваютс
|
||||
- каждая итоговая speaker label принадлежит `1..4`;
|
||||
- число и суммарная длительность интервалов по каждому каналу;
|
||||
- число коротких интервалов, overlap ratio, unmatched words, малые/остаточные
|
||||
кластеры и mapped purity относительно свежей опорной разметки;
|
||||
кластеры и mapped purity для фрагментов с опорной разметкой;
|
||||
- wall отдельно для model inference и segment post-processing.
|
||||
|
||||
Любой неверный shape/value/label или эффективная конфигурация, отличная от
|
||||
@@ -380,17 +390,17 @@ RSS выше 2 ГиБ на машине с 16 ГиБ требует отдель
|
||||
## Артефакты и приватность
|
||||
|
||||
Checkout и дерево сборки NeMo-Speech.cpp, скачанные веса и новое закрытое
|
||||
хранилище размещаются вне рабочей копии local-transcriber. Обычная `.scratch`
|
||||
внутри репозитория не подходит: `.gitignore` покрывает там только
|
||||
`.scratch/handoffs/`. Ни один runtime/model/build artifact или закрытый вход не
|
||||
попадает в Git.
|
||||
хранилище размещаются вне рабочей копии 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 и опорную разметку;
|
||||
- ASR-sidecar и имеющуюся опорную разметку;
|
||||
- raw per-fragment results, intervals, word assignments и probability
|
||||
diagnostics;
|
||||
- machine/power/runtime guards и stderr/stdout logs;
|
||||
@@ -458,8 +468,9 @@ privacy scan. Реализация стенда фиксируется чист
|
||||
|
||||
## Критерии завершения эксперимента
|
||||
|
||||
- [ ] Свежая опора подтверждена владельцем и не наследует старые контрольные
|
||||
значения.
|
||||
- [ ] Свежая опора собрана в новом закрытом хранилище; подтвержденные входы,
|
||||
доступная разметка и числа голосов связаны по хешам, а старые manifest,
|
||||
расписание и результаты не наследуются.
|
||||
- [ ] Runtime, модель, сборка и оба рецепта закреплены и проверены до моделей.
|
||||
- [ ] Clean local-transcriber source commit сохранен в закрытом хранилище и
|
||||
публичном отчете, а `--validate-only` проверяет его до моделей.
|
||||
|
||||
@@ -316,7 +316,7 @@ def _resolve(root: Path, value: Any) -> Path:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise PreflightError("unsafe-relative-path")
|
||||
pure = PurePosixPath(value)
|
||||
if pure.is_absolute() or ".." in pure.parts or "\\" in value:
|
||||
if pure.is_absolute() or ".." in pure.parts or "\\" in value or "\x00" in value:
|
||||
raise PreflightError("path-escape")
|
||||
base = root.resolve()
|
||||
resolved = (base / Path(value)).resolve()
|
||||
@@ -509,17 +509,22 @@ def validate_manifest(
|
||||
media_hash = _sha(record["sha256"], "record-hash")
|
||||
sidecar_hash = _sha(record["sidecar_sha256"], "sidecar-hash")
|
||||
if role == "fragment":
|
||||
_sha(record["reference_sha256"], "reference-hash")
|
||||
reference_path = record["reference_path"]
|
||||
reference_hash = record["reference_sha256"]
|
||||
if (reference_path is None) != (reference_hash is None):
|
||||
raise PreflightError("reference-binding")
|
||||
if reference_hash is not None:
|
||||
_sha(reference_hash, "reference-hash")
|
||||
fragment_hashes.append(media_hash)
|
||||
fragment_sidecars.append(sidecar_hash)
|
||||
if verify_files:
|
||||
_verify(root, record["path"], media_hash, record["size_bytes"])
|
||||
_verify(root, record["sidecar_path"], sidecar_hash, None)
|
||||
if role == "fragment":
|
||||
if role == "fragment" and reference_hash is not None:
|
||||
_verify(
|
||||
root,
|
||||
record["reference_path"],
|
||||
record["reference_sha256"],
|
||||
reference_path,
|
||||
reference_hash,
|
||||
None,
|
||||
)
|
||||
_validate_schedule(value["schedule"], fragment_hashes, fragment_sidecars)
|
||||
@@ -1090,8 +1095,6 @@ def memory_gate(
|
||||
for value in integer_values
|
||||
):
|
||||
raise ValueError("memory-value")
|
||||
if swap_before_bytes > 0 or swap_after_bytes > 0:
|
||||
return {"state": "stop", "reason": "active-swap", **base}
|
||||
if stage == "fragment":
|
||||
if len(rss_after_inputs) != 3:
|
||||
return {"state": "stop", "reason": "rss-no-plateau", **base}
|
||||
@@ -1441,13 +1444,10 @@ def _guard_transition_reasons(
|
||||
|
||||
|
||||
def _swap_active(before: Mapping[str, Any], after: Mapping[str, Any] | None = None) -> bool:
|
||||
if before["swap_used_bytes"] > 0:
|
||||
return True
|
||||
if after is None:
|
||||
return False
|
||||
return bool(
|
||||
after["swap_used_bytes"] > 0
|
||||
or after["swap_sin_bytes"] != before["swap_sin_bytes"]
|
||||
after["swap_sin_bytes"] != before["swap_sin_bytes"]
|
||||
or after["swap_sout_bytes"] != before["swap_sout_bytes"]
|
||||
)
|
||||
|
||||
|
||||
@@ -222,6 +222,7 @@ def test_load_speaker_diarizer_keeps_corrupt_cache_when_download_is_invalid(
|
||||
|
||||
with (
|
||||
patch("huggingface_hub.cached_assets_path", return_value=tmp_path),
|
||||
patch.dict(sys.modules, {"sherpa_onnx": SimpleNamespace()}),
|
||||
patch("httpx.stream", return_value=FakeResponse()),
|
||||
pytest.raises(RuntimeError, match="Контрольная сумма"),
|
||||
):
|
||||
|
||||
@@ -213,6 +213,60 @@ def test_preflight_validates_fresh_exact_schema_before_loading(tmp_path):
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_approved_fresh_basis_loads_models_after_preflight(tmp_path):
|
||||
manifest = _manifest(tmp_path)
|
||||
calls = []
|
||||
|
||||
outcome = experiment.preflight_and_load(
|
||||
manifest,
|
||||
root=tmp_path,
|
||||
environment_probe=_environment,
|
||||
w0_loader=lambda: calls.append("w0"),
|
||||
candidate_loader=lambda: calls.append("candidate"),
|
||||
)
|
||||
|
||||
assert outcome.state == "valid"
|
||||
assert calls == ["w0", "candidate"]
|
||||
|
||||
|
||||
def test_fragment_without_reference_loads_models_after_preflight(tmp_path):
|
||||
manifest = _manifest(tmp_path)
|
||||
fragment = manifest["inputs"][0]["fragment"]
|
||||
(tmp_path / fragment["reference_path"]).unlink()
|
||||
fragment["reference_path"] = None
|
||||
fragment["reference_sha256"] = None
|
||||
calls = []
|
||||
|
||||
outcome = experiment.preflight_and_load(
|
||||
manifest,
|
||||
root=tmp_path,
|
||||
environment_probe=_environment,
|
||||
w0_loader=lambda: calls.append("w0"),
|
||||
candidate_loader=lambda: calls.append("candidate"),
|
||||
)
|
||||
|
||||
assert outcome.state == "valid"
|
||||
assert calls == ["w0", "candidate"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["reference_path", "reference_sha256"])
|
||||
def test_fragment_rejects_half_present_reference_before_loaders(tmp_path, field):
|
||||
manifest = _manifest(tmp_path)
|
||||
manifest["inputs"][0]["fragment"][field] = None
|
||||
calls = []
|
||||
|
||||
with pytest.raises(experiment.PreflightError, match="reference-binding"):
|
||||
experiment.preflight_and_load(
|
||||
manifest,
|
||||
root=tmp_path,
|
||||
environment_probe=_environment,
|
||||
w0_loader=lambda: calls.append("w0"),
|
||||
candidate_loader=lambda: calls.append("candidate"),
|
||||
)
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_missing_owner_approval_is_needs_user_and_never_loads(tmp_path):
|
||||
manifest = _manifest(tmp_path)
|
||||
manifest["fresh_basis"]["approval"] = None
|
||||
@@ -291,6 +345,25 @@ def test_preflight_rejects_hash_path_and_symlink_escape_without_leaking_path(tmp
|
||||
experiment.validate_manifest(manifest, root=tmp_path, environment=_environment())
|
||||
|
||||
|
||||
def test_preflight_rejects_nul_path_without_leaking_or_loading(tmp_path):
|
||||
manifest = _manifest(tmp_path)
|
||||
manifest["inputs"][0]["source_path"] = "private\x00source.wav"
|
||||
calls = []
|
||||
|
||||
with pytest.raises(experiment.PreflightError) as raised:
|
||||
experiment.preflight_and_load(
|
||||
manifest,
|
||||
root=tmp_path,
|
||||
environment_probe=_environment,
|
||||
w0_loader=lambda: calls.append("w0"),
|
||||
candidate_loader=lambda: calls.append("candidate"),
|
||||
)
|
||||
|
||||
assert str(raised.value) == "path-escape"
|
||||
assert "private" not in str(raised.value)
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_preflight_rejects_legacy_cell_names_roots_and_recipe_pin_mismatch(tmp_path):
|
||||
manifest = _manifest(tmp_path)
|
||||
manifest["schedule"][0]["semantic_id"] = "41-W0"
|
||||
@@ -701,7 +774,7 @@ def test_memory_gate_uses_frozen_plateau_and_linear_growth_policy_boundaries():
|
||||
peak_rss_bytes=300 * mib,
|
||||
rss_after_inputs=[100 * mib, 120 * mib, 121 * mib],
|
||||
machine_ram_bytes=16 * 1024**3,
|
||||
)["reason"] == "active-swap"
|
||||
)["reason"] is None
|
||||
assert experiment.memory_gate(
|
||||
stage="fragment",
|
||||
oom=True,
|
||||
@@ -713,6 +786,23 @@ def test_memory_gate_uses_frozen_plateau_and_linear_growth_policy_boundaries():
|
||||
)["reason"] == "oom"
|
||||
|
||||
|
||||
def test_memory_gate_allows_preoccupied_swap() -> None:
|
||||
mib = 1024**2
|
||||
|
||||
result = experiment.memory_gate(
|
||||
stage="fragment",
|
||||
oom=False,
|
||||
swap_before_bytes=1_730_000_000,
|
||||
swap_after_bytes=1_730_000_000,
|
||||
peak_rss_bytes=300 * mib,
|
||||
rss_after_inputs=[100 * mib, 120 * mib, 121 * mib],
|
||||
machine_ram_bytes=16 * 1024**3,
|
||||
)
|
||||
|
||||
assert result["state"] == "pass"
|
||||
assert result["reason"] is None
|
||||
|
||||
|
||||
def test_memory_gate_marks_two_gibibyte_review_as_needs_user():
|
||||
result = experiment.memory_gate(
|
||||
stage="fragment",
|
||||
@@ -1011,7 +1101,7 @@ def test_guarded_cell_accepts_stable_manifest_declared_performance_mode():
|
||||
assert len(result["attempts"]) == 1
|
||||
|
||||
|
||||
def test_second_guard_invalidation_stops_with_first_reason_and_active_swap_never_retries():
|
||||
def test_second_guard_invalidation_stops_with_first_reason():
|
||||
throttled = iter(
|
||||
[
|
||||
_guard(),
|
||||
@@ -1031,17 +1121,33 @@ def test_second_guard_invalidation_stops_with_first_reason_and_active_swap_never
|
||||
assert second["reason"] == "new-throttling"
|
||||
assert len(second["attempts"]) == 2
|
||||
|
||||
calls = []
|
||||
swapped = experiment.run_guarded_cell(
|
||||
|
||||
def test_guarded_cell_allows_preoccupied_swap_when_counters_stay_stable():
|
||||
snapshots = iter(
|
||||
[
|
||||
_guard(
|
||||
swap_used_bytes=1_730_000_000,
|
||||
swap_sin_bytes=100,
|
||||
swap_sout_bytes=200,
|
||||
),
|
||||
_guard(
|
||||
swap_used_bytes=1_730_000_000,
|
||||
swap_sin_bytes=100,
|
||||
swap_sout_bytes=200,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
result = experiment.run_guarded_cell(
|
||||
{"semantic_id": "safe-cell", "recipe": "W0-FRESH"},
|
||||
runner=lambda _request: calls.append("run") or {"wall_seconds": 1.0},
|
||||
guard_reader=lambda: _guard(swap_used_bytes=1),
|
||||
stabilize=lambda: calls.append("stabilize"),
|
||||
runner=lambda _request: {"wall_seconds": 1.0},
|
||||
guard_reader=lambda: next(snapshots),
|
||||
stabilize=lambda: None,
|
||||
expected_machine=_machine(),
|
||||
)
|
||||
assert swapped["state"] == "stop"
|
||||
assert swapped["reason"] == "active-swap"
|
||||
assert calls == []
|
||||
|
||||
assert result["state"] == "accepted"
|
||||
assert len(result["attempts"]) == 1
|
||||
|
||||
|
||||
def test_oom_stops_without_retry_and_private_result_is_written_atomically(tmp_path):
|
||||
@@ -1123,8 +1229,9 @@ def test_guarded_cell_does_not_retry_hard_mismatch_and_sanitizes_exceptions():
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_guarded_cell_hashes_result_rejected_by_after_run_swap():
|
||||
snapshots = iter([_guard(), _guard(swap_sin_bytes=1)])
|
||||
@pytest.mark.parametrize("counter", ["swap_sin_bytes", "swap_sout_bytes"])
|
||||
def test_guarded_cell_hashes_result_rejected_by_after_run_swap(counter):
|
||||
snapshots = iter([_guard(), _guard(**{counter: 1})])
|
||||
raw_result = {"wall_seconds": 1.0, "private": "not-published"}
|
||||
result = experiment.run_guarded_cell(
|
||||
{"semantic_id": "safe-cell", "recipe": "W0-FRESH"},
|
||||
|
||||
Reference in new issue
Block a user