From a8b8a8c0c9c1689a7ed43fb974f74af3795fd54e Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Mon, 21 Sep 2026 23:20:51 +0300 Subject: [PATCH] =?UTF-8?q?fix(cuda):=20=D1=83=D1=87=D1=82=D1=91=D0=BD=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BA=D0=B0=D0=B7=20=D0=BF=D0=BE=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D0=B5=D0=BC=D0=BE=D0=BC=D1=83=20=D1=82=D0=B8=D0=BF=D1=83=20?= =?UTF-8?q?=D0=B2=D1=8B=D1=87=D0=B8=D1=81=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++-- docs/adr/001-cuda-bootstrap.md | 13 +++++--- src/local_transcriber/cli.py | 13 ++++---- src/local_transcriber/transcriber.py | 10 ++++++- tests/test_cli.py | 44 ++++++++++++++++++++++++++-- 5 files changed, 70 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 31e6318..f00e180 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,9 @@ device = "cuda" ``` Установка extra сама по себе не меняет `auto`: он всегда использует ONNX CPU. -Явная CUDA при ошибке завершает обработку с исходной причиной и подсказкой, -без молчаливого перехода на CPU. В батче ошибка учитывается для каждого файла. +Явная CUDA при ошибке завершает обработку с исходной причиной; для распознанных +причин выводится подсказка. Молчаливого перехода на CPU нет. В батче ошибка +учитывается для каждого файла. На Windows нужен также [Visual C++ Runtime x64](https://aka.ms/vs/17/release/vc_redist.x64.exe). Состав DLL проверен для закреплённых версий CTranslate2 и cuBLAS; diff --git a/docs/adr/001-cuda-bootstrap.md b/docs/adr/001-cuda-bootstrap.md index eef6fdc..e3f6e72 100644 --- a/docs/adr/001-cuda-bootstrap.md +++ b/docs/adr/001-cuda-bootstrap.md @@ -59,7 +59,9 @@ preload всё равно выполняется до создания CUDA-мо Явный `device` из CLI или TOML передаёт `strict_device=True` при загрузке и транскрипции. CUDA не заменяется молча на CPU. Подсказки различают отсутствие библиотек, проблемы драйвера, несовместимость GPU/runtime и нехватку памяти; -исходное сообщение сохраняется. Неизвестная ошибка не получает произвольный +отказ по неподдерживаемому типу вычислений учитывает устройство, чтобы +одинаковая ошибка CTranslate2 на CPU не получала CUDA-подсказку. Исходное +сообщение сохраняется. Неизвестная ошибка не получает произвольный совет установить Toolkit. Это одинаково для одного файла и батча. Прежний механизм fallback оркестратора при `strict_device=False` сохранён @@ -82,8 +84,11 @@ preload всё равно выполняется до создания CUDA-мо - `nvidia_cublas_cu12-12.9.1.4-py3-none-win_amd64.whl`, SHA-256 `1e5fee10662e6e52bd71dec533fbbd4971bb70a5f24f3bc3793e5c2e9dc640bf`. DLL находятся в `nvidia/cublas/bin`. cuBLAS импортирует cuBLAS Lt и - KERNEL32; cuBLAS Lt импортирует KERNEL32. `nvblas64_12.dll` не требуется - проверенному пути CTranslate2. + KERNEL32; cuBLAS Lt импортирует KERNEL32. В строках cuBLAS Lt также найдена + динамическая ссылка на `nvrtc64_120_0.dll`, которой нет в PE imports. + Обязательность NVRTC для Whisper этим статическим анализом не установлена; + первый реальный Windows-прогон должен проверить и этот путь загрузки. + `nvblas64_12.dll` не требуется проверенному пути CTranslate2. Основания: [CTranslate2 4.8.1 на PyPI](https://pypi.org/project/ctranslate2/4.8.1/#files), [cuBLAS 12.9.1.4 на PyPI](https://pypi.org/project/nvidia-cublas-cu12/12.9.1.4/#files), @@ -97,7 +102,7 @@ Windows API не заменяют такую проверку. ## Проверка поставки и поведения -На Linux выполнены `uv run pytest`: 500 passed, 2 skipped. Пропуски — +На Linux выполнены `uv run pytest`: 524 passed, 2 skipped. Пропуски — Windows-only кодировка OEM и интеграционный bootstrap без optional-пакета. До удаления cuBLAS интеграционный тест Linux bootstrap проходил. Обычный `uv sync --locked` удаляет прежнюю обязательную cuBLAS; diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index b6541bd..eed8f4d 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -65,9 +65,9 @@ app = typer.Typer() console = Console(stderr=True) -def _print_cuda_hint(exc: BaseException) -> None: +def _print_cuda_hint(exc: BaseException, device: str | None = None) -> None: """Показывает подсказку, не заменяя исходную ошибку.""" - hint = cuda_error_hint(exc) + hint = cuda_error_hint(exc, device=device) if hint: console.print(hint, style="yellow", markup=False) @@ -348,6 +348,7 @@ def main( ) raise SystemExit(2) + resolved_device: str | None = None try: config = load_config() cli_values: CliValues = { @@ -405,15 +406,15 @@ def main( except SystemExit: raise except ValueError as exc: - _print_cuda_hint(exc) + _print_cuda_hint(exc, resolved_device) console.print(f"Ошибка: {exc}", style="red bold") raise SystemExit(1) except (FileNotFoundError,) as exc: - _print_cuda_hint(exc) + _print_cuda_hint(exc, resolved_device) console.print(f"Ошибка: {exc}", style="red bold") raise SystemExit(1) except Exception as exc: - _print_cuda_hint(exc) + _print_cuda_hint(exc, resolved_device) if verbose: console.print_exception() else: @@ -759,7 +760,7 @@ def _run_batch( except KeyboardInterrupt: raise except Exception as exc: - _print_cuda_hint(exc) + _print_cuda_hint(exc, actual_device) if verbose: console.print_exception() else: diff --git a/src/local_transcriber/transcriber.py b/src/local_transcriber/transcriber.py index 12e647b..a516485 100644 --- a/src/local_transcriber/transcriber.py +++ b/src/local_transcriber/transcriber.py @@ -195,9 +195,17 @@ def _is_cuda_error(exc: BaseException) -> bool: return any(k in msg for k in ("cuda", "cublas", "cudnn", "out of memory")) -def cuda_error_hint(exc: BaseException) -> str | None: +def cuda_error_hint(exc: BaseException, *, device: str | None = None) -> str | None: """Подсказывает действие только для распознанной причины CUDA-ошибки.""" msg = str(exc).lower() + if device == "cuda" and "requested " in msg and "compute type" in msg and ( + "do not support efficient" in msg + ): + return ( + "Тип вычислений несовместим с выбранным GPU/CUDA runtime. " + "Установка extra не добавит аппаратную поддержку. Выберите " + "поддерживаемый --compute-type или --device onnx / --device cpu." + ) if any(k in msg for k in ( "no kernel image", "invalid device function", "unsupported gpu", "cublas_status_arch_mismatch", "cuda_error_no_binary_for_gpu", diff --git a/tests/test_cli.py b/tests/test_cli.py index 1586a41..72627d8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -805,14 +805,54 @@ def test_default_cli_stays_on_onnx_with_nvidia_driver(tmp_path, monkeypatch): patch("shutil.which", return_value="/usr/bin/nvidia-smi"), patch("local_transcriber.transcriber.get_backend", return_value=backend) as get_backend, patch("local_transcriber.cli.write_transcript"), - patch("local_transcriber._cuda_bootstrap.ensure_cublas_loadable") as bootstrap, ): out = runner.invoke(app, [str(audio)]) assert out.exit_code == 0, out.output get_backend.assert_called_once_with("onnx", compute_type_explicit=False) assert backend.ensure_model_available.call_args.args[:2] == ("gigaam-v3-e2e-rnnt", "int8") - bootstrap.assert_not_called() + + +@pytest.mark.parametrize("from_config", [False, True]) +@pytest.mark.parametrize("phase", ["load", "single", "batch"]) +@pytest.mark.parametrize("device", ["cuda", "cpu"]) +@pytest.mark.parametrize("compute_type", ["float16", "int8"]) +def test_unsupported_compute_type_hint_requires_cuda_context( + tmp_path, monkeypatch, from_config, phase, device, compute_type +): + """Одинаковая ошибка CTranslate2 получает CUDA-подсказку только на CUDA.""" + monkeypatch.chdir(tmp_path) + files = [tmp_path / "one.wav"] + if phase == "batch": + files.append(tmp_path / "two.wav") + for file in files: + file.write_bytes(b"audio") + args = [str(file) for file in files] + if from_config: + (tmp_path / ".transcriber.toml").write_text( + f'device = "{device}"\ncompute_type = "{compute_type}"\n' + ) + else: + args += ["--device", device, "--compute-type", compute_type] + message = ( + f"Requested {compute_type} compute type, but the target device or backend " + f"do not support efficient {compute_type} computation." + ) + backend = _make_backend() + error = ValueError(message) + if phase == "load": + backend.create_model.side_effect = error + else: + backend.transcribe.side_effect = error + + with patch("local_transcriber.transcriber.get_backend", return_value=backend): + out = runner.invoke(app, args) + + assert out.exit_code == 1 + assert message in " ".join(out.output.split()) + assert ("Тип вычислений несовместим" in out.output) is (device == "cuda") + assert "uv sync --extra cuda" not in out.output + assert "Переключение на CPU" not in out.output def test_cli_device_fallback_warning(tmp_path):