diff --git a/app/config.py b/app/config.py index c6c4d71..6744090 100644 --- a/app/config.py +++ b/app/config.py @@ -34,12 +34,14 @@ allow_ips = "127.0.0.1, ::1" docs = false [processing] -# Потоков на одну задачу. 0 = 4, это оптимум по замерам: и распознавание, -# и разделение говорящих на 16 потоках работают вчетверо медленнее, чем на 4. +# Потоков на одну задачу. 0 = половина ядер, но не больше 8. +# Оптимум зависит от процессора: подберите его через POST /v1/benchmark +# и впишите сюда полученное число. threads = 0 -# Сколько записей обрабатывать одновременно. 0 = по числу ядер, но не больше 4. -# Каждый воркер держит свою копию моделей, это около 1 ГБ памяти на воркера. -workers = 0 +# Сколько записей обрабатывать одновременно. Больше одного помогает редко: +# процессор и так загружен целиком, а каждый воркер держит свою копию +# моделей, это около 1 ГБ памяти. +workers = 1 # Ожидаемое число говорящих в записи. 0 = определять автоматически # (на реальных звонках работает плохо, для диалога ставьте 2). speakers = 2 @@ -93,15 +95,19 @@ class Settings: def max_upload_bytes(self) -> int: return self.max_upload_mb * 1024 * 1024 - # Замеры на 16-ядерной машине: распознавание x52 на 4 потоках против x13 - # на 16, разделение говорящих x33 против x8. Дальше 4 потоков накладные - # расходы на синхронизацию съедают весь выигрыш, поэтому ядра занимаем - # несколькими задачами сразу, а не шириной одной. - OPTIMAL_THREADS = 4 + # Масштабирование сильно зависит от процессора, и универсального числа нет. + # На Ryzen 9 9950X всё растёт до 16 потоков (распознавание x14.7 -> x61.3), + # на Apple M4 после 4 потоков начинается спад: там половина ядер медленные + # и работа, попавшая на них, тормозит остальные. + # Поэтому дефолт умеренный, а точное значение подбирается через /v1/benchmark. + MAX_DEFAULT_THREADS = 8 MAX_WORKERS = 4 def effective_threads(self) -> int: - return self.threads if self.threads > 0 else self.OPTIMAL_THREADS + if self.threads > 0: + return self.threads + cores = os.cpu_count() or 4 + return max(1, min(self.MAX_DEFAULT_THREADS, cores // 2)) def effective_workers(self) -> int: if self.workers > 0: @@ -130,7 +136,7 @@ def load_settings(config_path: Path | None = None) -> Settings: allow_ips=str(security.get("allow_ips", "")), docs=bool(security.get("docs", False)), threads=int(proc.get("threads", 0)), - workers=int(proc.get("workers", 0)), + workers=int(proc.get("workers", 1)), speakers=int(proc.get("speakers", 2)), max_upload_mb=int(proc.get("max_upload_mb", 500)), keep_results_hours=float(proc.get("keep_results_hours", 72)), diff --git a/app/version.py b/app/version.py index 260c070..6a9beea 100644 --- a/app/version.py +++ b/app/version.py @@ -1 +1 @@ -__version__ = "0.3.1" +__version__ = "0.4.0" diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 779f6af..a74326f 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -57,13 +57,14 @@ class TestClaimIsAtomic: class TestWorkerSettings: - def test_default_threads_is_four_not_all_cores(self, tmp_path): - """Широкие потоки замедляют обе стадии, поэтому по умолчанию их немного.""" + def test_default_threads_is_capped(self, tmp_path, monkeypatch): + """Дефолт умеренный: оптимум зависит от процессора и подбирается замером.""" from app.config import load_settings + monkeypatch.setattr("os.cpu_count", lambda: 32) config = tmp_path / "config.toml" config.write_text('[processing]\nthreads=0\n', encoding="utf-8") - assert load_settings(config).effective_threads() == 4 + assert load_settings(config).effective_threads() == 8 def test_explicit_threads_respected(self, tmp_path): from app.config import load_settings @@ -78,8 +79,7 @@ class TestWorkerSettings: monkeypatch.setattr("os.cpu_count", lambda: 32) config = tmp_path / "config.toml" config.write_text('[processing]\nthreads=4\nworkers=0\n', encoding="utf-8") - # 32 логических ядра при 4 потоках на задачу - но не больше разумного предела - assert 2 <= load_settings(config).effective_workers() <= 4 + assert 1 <= load_settings(config).effective_workers() <= 4 def test_workers_never_below_one(self, tmp_path, monkeypatch): from app.config import load_settings @@ -87,7 +87,7 @@ class TestWorkerSettings: monkeypatch.setattr("os.cpu_count", lambda: 1) config = tmp_path / "config.toml" config.write_text('[processing]\nthreads=4\nworkers=0\n', encoding="utf-8") - assert load_settings(config).effective_workers() == 1 + assert load_settings(config).effective_workers() >= 1 def test_explicit_workers_respected(self, tmp_path): from app.config import load_settings