diff --git a/README.md b/README.md index f6299f9..f00e180 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ transcribe meeting.mp4 ``` - **Полностью локально** — данные не покидают машину -- **Авто-ускорение** — NVIDIA CUDA при наличии GPU, иначе ONNX на CPU +- **ONNX на CPU по умолчанию** — CUDA и OpenVINO включаются явно - **Батч-режим** — обработка нескольких файлов за один вызов - **Разделение говорящих** — локальная диаризация по флагу `--diarize` - **Из проводника Windows** — пункт Transcribe в меню «Отправить» ([установка](#контекстное-меню-проводника-windows)) @@ -50,18 +50,16 @@ transcribe --install-menu Подробнее — в разделе [контекстное меню проводника](#контекстное-меню-проводника-windows). -**4. Ускорение (ставится автоматически):** +**4. Выбор исполнения:** -- **NVIDIA CUDA** (GPU): если есть GPU — транскрипция в 5-10× быстрее - - **Windows**: `winget install -e --id Nvidia.CUDA --version 12.9` (от администратора), перезапустить терминал - - **Linux / WSL2**: работает из коробки (нужен только драйвер: `nvidia-smi`) -- **Без NVIDIA GPU**: автоматически используется ONNX с GigaAM RNN-T на CPU. - Эта модель понимает только русскую речь -- **Другие языки без NVIDIA**: выбирайте Whisper явно — - `--device openvino-cpu --model medium` на x86 или `--device cpu --model medium` - на любой платформе -- **OpenVINO** для Intel GPU или x86 CPU остаётся доступен через явный - `--device openvino`, `--device openvino-gpu` или `--device openvino-cpu` +- По умолчанию используется ONNX с GigaAM RNN-T на CPU, независимо от наличия + NVIDIA. Эта модель понимает только русскую речь. +- Для других языков выбирайте Whisper явно: + `--device openvino-cpu --model medium` на x86 или `--device cpu --model medium`. +- OpenVINO для Intel GPU или x86 CPU доступен через `--device openvino`, + `--device openvino-gpu` или `--device openvino-cpu`. +- Для NVIDIA установите дополнительные библиотеки и выберите CUDA явно: + [подключение CUDA](#подключение-cuda). **5. Готово:** @@ -132,17 +130,74 @@ HuggingFace Hub использует симлинки для экономии м +## Подключение CUDA + +Обычная установка не требует NVIDIA-пакетов. Extra `cuda` добавляет cuBLAS +в окружение приложения на Linux/WSL x86_64 и Windows x64. Полный системный +CUDA Toolkit и ручная правка системного PATH для этого пути не нужны. +Совместимые GPU и драйвер NVIDIA необходимы отдельно: extra не устанавливает +драйвер и не исправляет несовместимость старой видеокарты. + +Для установки из Git или подключения CUDA к уже установленной программе: + +```bash +uv tool install --python 3.13 --force "local-transcriber[cuda] @ git+https://git.dementev.space/ddmitry/local-transcriber.git" +``` + +Из клона репозитория: + +```bash +uv tool install --python 3.13 --force ".[cuda]" +``` + +Для локальной разработки: + +```bash +uv sync --extra cuda +uv run --extra cuda transcribe meeting.mp4 --device cuda +``` + +После установки extra выберите CUDA при запуске: + +```bash +transcribe meeting.mp4 --device cuda +``` + +Либо задайте в `.transcriber.toml`: + +```toml +device = "cuda" +``` + +Установка extra сама по себе не меняет `auto`: он всегда использует ONNX CPU. +Явная CUDA при ошибке завершает обработку с исходной причиной; для распознанных +причин выводится подсказка. Молчаливого перехода на CPU нет. В батче ошибка +учитывается для каждого файла. + +На Windows нужен также [Visual C++ Runtime x64](https://aka.ms/vs/17/release/vc_redist.x64.exe). +Состав DLL проверен для закреплённых версий CTranslate2 и cuBLAS; +реальный прогон нового пути на Windows без Toolkit пока не выполнен. +Подробности и ограничения проверки — [ADR-001](docs/adr/001-cuda-bootstrap.md). + +При обновлении из старой установки: + +- Для CPU повторите обычную команду `uv tool install` с `--force` без `[cuda]`. + В окружении разработки выполните `uv sync` — прежняя обязательная cuBLAS будет удалена. +- Для CUDA повторите соответствующую команду выше. Старый автоматический выбор + по `nvidia-smi` больше не действует; сохраните `device = "cuda"` в конфиге + либо передавайте `--device cuda`. + ## Использование ```bash -# Простой запуск (CUDA medium или ONNX GigaAM RNN-T, язык ru) +# Простой запуск (ONNX GigaAM RNN-T на CPU, язык ru) transcribe meeting.mp4 # Указать язык -transcribe lecture.mp3 --language en +transcribe lecture.mp3 --device cpu --model medium --language en # Максимальное качество на NVIDIA GPU -transcribe podcast.wav --model large-v3 --compute-type float16 +transcribe podcast.wav --device cuda --model large-v3 --compute-type float16 # Максимальное качество на Intel GPU transcribe podcast.wav --model large-v3 --device openvino-gpu @@ -243,7 +298,7 @@ transcribe --uninstall-menu | Опция | Сокращение | По умолчанию | Описание | |-------|-----------|-------------|----------| | `--model` | `-m` | medium (CUDA) / gigaam-v3-e2e-rnnt (ONNX) | Модель распознавания | -| `--language` | `-l` | `ru` | Язык (ru, en, auto и др.); автоматический профиль без NVIDIA понимает только русский | +| `--language` | `-l` | `ru` | Язык (ru, en, auto и др.); автоматический профиль понимает только русский | | `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу | | `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu, onnx) | | `--compute-type` | — | float16 (CUDA) / int8 (ONNX/OpenVINO) / float32 (CPU) | Тип вычислений | @@ -260,7 +315,7 @@ transcribe --uninstall-menu | CPU через ONNX | ✅ авто | ✅ авто | ✅ авто | | OpenVINO (x86 CPU) | ✅ явно | — | ✅ явно | | OpenVINO (Intel GPU) | ✅ явно | — | ✅ явно | -| GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) | +| GPU (NVIDIA) | extra + явно (x86_64) | — | extra + явно (x64; прогон без Toolkit ожидается) | Данные по macOS основаны на доступности пакетов onnxruntime: прогонов на этой платформе не было. @@ -269,9 +324,9 @@ transcribe --uninstall-menu Linux / WSL2 - **Intel GPU** (Arc, встроенная графика) работает из коробки через OpenVINO -- **NVIDIA GPU** работает из коробки — cuBLAS ставится автоматически как зависимость -- Нужен только драйвер NVIDIA (проверка: `nvidia-smi`) -- На ARM (aarch64) cuBLAS через pip недоступен — нужен системный CUDA toolkit +- **NVIDIA GPU**: [extra `cuda` и явный выбор](#подключение-cuda), отдельно совместимый драйвер +- `nvidia-smi` проверяет наличие драйвера, но не доказывает совместимость GPU с runtime +- CUDA extra на ARM (aarch64) этим проектом не поддерживается @@ -288,13 +343,9 @@ transcribe --uninstall-menu - CPU работает из коробки - **Intel GPU** (Arc, встроенная графика) работает из коробки через OpenVINO -- Для **NVIDIA GPU** нужен **CUDA 12** (ctranslate2 4.7 не совместим с CUDA 11 и 13): - ``` - winget install -e --id Nvidia.CUDA --version 12.9 - ``` - > `winget install` требует запуска от имени администратора (elevated terminal). - > `uv tool install` работает без админа (ставит в пользовательскую директорию). -- После установки CUDA перезапустите терминал +- **NVIDIA GPU**: [extra `cuda` и явный выбор](#подключение-cuda), отдельно совместимый драйвер +- Требуется Visual C++ Runtime x64; системный CUDA Toolkit не нужен для extra +- Реальная транскрипция с extra без Toolkit пока не проверена на Windows @@ -318,8 +369,8 @@ diarize = true `--diarize` и `--no-diarize` позволяют переопределить `diarize` из конфига для отдельного запуска. -При `device = "auto"` выбирается CUDA, если доступен `nvidia-smi`, иначе ONNX. -Явный `device` из CLI или конфига отключает этот автоматический выбор. +При `device = "auto"` всегда выбирается ONNX CPU, даже если доступны +`nvidia-smi` и extra `cuda`. Другие варианты выбираются явно из CLI или конфига. Каталоги моделей различаются между бэкендами, поэтому при закреплении `model` в конфиге рекомендуется явно закрепить и совместимый `device`. То же с языком: автоматический ONNX-профиль рассчитан на русскую речь, а для остальных языков @@ -336,9 +387,9 @@ diarize = true ## Модели и GPU Рекомендации: -- **По умолчанию без CUDA:** ONNX `gigaam-v3-e2e-rnnt` — читаемый русский текст с +- **По умолчанию:** ONNX `gigaam-v3-e2e-rnnt` — читаемый русский текст с пунктуацией почти без потери скорости относительно сырого `gigaam-v3` -- **Макс. качество (NVIDIA):** `large-v3` + `--compute-type float16` +- **Макс. качество (NVIDIA):** `--device cuda --model large-v3 --compute-type float16` - **Макс. качество (Intel GPU):** `large-v3` + `--device openvino-gpu` - **Макс. скорость CPU (русский):** `--device onnx --model gigaam-v3` (17-29× RTF, без пунктуации; рекомендуется LLM-нормализация терминов после) - **Быстрый OpenVINO с низким WER:** `--device openvino-cpu --model large-v3-turbo --compute-type int8` (7,2× RTFx на контрольном Intel CPU; пунктуация может быть слабой) diff --git a/docs/PRD.md b/docs/PRD.md index 199f122..11bddcf 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -49,7 +49,7 @@ transcribe <путь_к_файлу> [опции] --output, -o Путь к выходному файлу По умолчанию: -transcript.md --device, -d Устройство (auto|cpu|cuda|onnx|openvino|openvino-gpu|openvino-cpu) - По умолчанию: auto (CUDA при наличии, иначе ONNX CPU) + По умолчанию: auto (всегда ONNX CPU) --compute-type Тип вычислений По умолчанию: float16 (CUDA) / int8 (ONNX) --verbose, -v Подробный вывод (прогресс сегментов) @@ -103,7 +103,7 @@ transcribe <путь_к_файлу> [опции] |---------------------------|---------------------------------------| | RTX 3060 + large-v3 | ~10-15x (1 час аудио ≈ 4-6 мин) | | RTX 4050 + large-v3 | ~12-18x (1 час аудио ≈ 3-5 мин) | -| Quadro M3000M + large-v3 | ~3-5x (1 час аудио ≈ 12-20 мин) | +| Quadro M3000M + CUDA | Не поддерживается текущей сборкой; см. [совместимость](gpu.md#совместимость-gpu) | | CPU + ONNX GigaAM RNN-T | ~10-14x (1 час аудио ≈ 4-6 мин) | | CPU + OpenVINO Turbo INT8 | ~7-10x (1 час аудио ≈ 6-9 мин) | | CPU (modern) + large-v3 | ~0.5-1x (1 час аудио ≈ 60-120 мин) | @@ -115,7 +115,7 @@ transcribe <путь_к_файлу> [опции] |-------------------|-------|--------------------------|----------------------------|-------------------------| | RTX 3060 | 6 GB | ✅ | ✅ (впритык) | int8 — безопасный выбор | | RTX 4050 | 6 GB | ✅ | ✅ (впритык) | int8 — безопасный выбор | -| Quadro M3000M | 4 GB | ✅ | ⚠️ может OOM | int8 обязательно | +| Quadro M3000M | 4 GB | ❌ | ❌ | ONNX CPU или OpenVINO | | Без GPU | — | ONNX GigaAM INT8 | — | auto выбирает ONNX | Device-aware дефолты выбирают `float16` для CUDA и `int8` для ONNX/OpenVINO. @@ -123,7 +123,7 @@ Device-aware дефолты выбирают `float16` для CUDA и `int8` д ### 4.2. Требования к окружению - Python ≥ 3.13 -- Для GPU: Linux/WSL2 — cuBLAS из nvidia-cublas-cu12 (ставится автоматически через `uv sync`); Windows — системный CUDA toolkit (см. ADR-001) +- Для NVIDIA GPU на Linux/WSL x86_64 и Windows x64: совместимый драйвер, extra `cuda` и явный выбор CUDA (см. ADR-001); Windows также требует Visual C++ Runtime x64 - Дисковое пространство для моделей: зависит от выбранного backend и модели - Выходные файлы: UTF-8 (явная кодировка при записи) @@ -177,9 +177,9 @@ local-transcriber/ | Качество распознавания русского текста | Среднее | large-v3 хорошо справляется с ru; при проблемах — попробовать `--language ru` вместо auto | | Нет разделения по спикерам | Низкое | Осознанно выведено за скоуп MVP; добавление diarization (pyannote.audio) — возможное расширение | | Первый запуск: долгая загрузка модели | Низкое | Прогресс-бар при скачивании; модели кешируются в `~/.cache/huggingface/` | -| CUDA несовместимость на Windows | Среднее | Автоматический fallback на CPU + предупреждение; в README — инструкция по CUDA | +| CUDA несовместимость на Windows | Среднее | Явная CUDA сохраняет причину ошибки; пользователь выбирает CPU или исправляет окружение по README | | Большие файлы (>2 часов) | Среднее | Учитывать память выбранного backend; чанкование рассматривается отдельно | -| OOM на GPU с 4 GB VRAM | Среднее | CUDA использует float16; при OOM — fallback на CPU с предупреждением или явный более лёгкий профиль | +| OOM на GPU с 4 GB VRAM | Среднее | CUDA использует float16; при OOM явной CUDA — ошибка с подсказкой выбрать меньшую модель или CPU | | Файл без речи (тишина, музыка, шум) | Низкое | Создаётся транскрипт с шапкой метаданных и `*Речь не обнаружена.*` в теле + предупреждение в stderr | ## 7. Вне скоупа MVP diff --git a/docs/adr/001-cuda-bootstrap.md b/docs/adr/001-cuda-bootstrap.md index f303306..e3f6e72 100644 --- a/docs/adr/001-cuda-bootstrap.md +++ b/docs/adr/001-cuda-bootstrap.md @@ -1,78 +1,134 @@ -# ADR-001: Preload libcublas из pip-пакета для GPU на Linux/WSL2 +# ADR-001: Необязательная CUDA и загрузка cuBLAS из окружения -**Статус**: Принято +**Статус**: Принято; пересмотр обязательной cuBLAS от 2026-03-18 **Дата**: 2026-03-18 +**Обновлено**: 2026-09-21, задача [#2](https://git.dementev.space/ddmitry/local-transcriber/issues/2) ## Контекст -ctranslate2 (backend faster-whisper) в runtime делает `dlopen("libcublas.so.12")`, -но не бандлит эту библиотеку в свой wheel — ожидает её в системе. -Без установленного CUDA toolkit `uv run transcribe --device cuda` падает с ошибкой. +CTranslate2 загружает cuBLAS динамически и не поставляет её в своём wheel. +Прежняя обязательная зависимость добавляла около 581 MB скачивания на Linux +x86_64, даже если пользователь работал только через ONNX CPU. +Наличие `nvidia-smi` доказывает наличие утилиты драйвера, но не совместимость GPU +с runtime. Автоматический выбор CUDA мог приводить к ошибке на старом GPU. -Ключевые факты: -- `libcuda.so.1` приходит от NVIDIA driver (всегда есть, если GPU есть) -- `libcublas.so.12` отсутствует в wheel ctranslate2 на обеих платформах -- **cuDNN не нужен** — в `libctranslate2.so` ноль символов cudnn (проверено `nm -D` и `strings`) -- На Windows ctranslate2 делает `os.add_dll_directory` в своём `__init__.py`, - но только для DLL внутри пакета; `cublas64_12.dll` тоже не бандлится +Прежнее утверждение о недоступности Windows wheel `nvidia-cublas-cu12` было +ошибочным: такой wheel есть и для закреплённой версии 12.9.1.4. Следовательно, +системный CUDA Toolkit не должен быть обязательным способом подключения Windows. ## Решение -### nvidia-cublas-cu12 как pip-зависимость +CUDA подключается через extra `cuda` на Linux/WSL x86_64 и Windows x64. +Драйвер NVIDIA остаётся внешним требованием; Windows также требует Visual C++ +Runtime x64. Установка extra не включает CUDA: пользователь выбирает её через +CLI или TOML. `auto` всегда выбирает ONNX CPU, ASR и VAD получают явный +`CPUExecutionProvider`. Команды установки и обновления — в +[README](../../README.md#подключение-cuda). -В `pyproject.toml`: -``` -"nvidia-cublas-cu12>=12.4; sys_platform == 'linux' and platform_machine == 'x86_64'" -``` +CTranslate2 4.8.1 и cuBLAS 12.9.1.4 закреплены в `pyproject.toml`, чтобы +установка через `uv tool` и из клона использовала проверенное сочетание. +В cuBLAS 12.9.2.10 появилась дополнительная зависимость NVRTC. Обновление +версий требует повторной проверки библиотек и bootstrap; одного обновления +lock-файла недостаточно. -Нижняя граница `>=12.4` — ctranslate2 собран с CUDA 12.4. Пакет доступен только -для Linux x86_64 (на Windows и macOS не устанавливается по platform marker). +### Linux: preload по полному пути -### ctypes.CDLL preload вместо LD_LIBRARY_PATH +`_cuda_bootstrap.py` загружает `libcublas.so.12` через `ctypes.CDLL` с +`RTLD_GLOBAL`. Линкер затем разрешает запрос CTranslate2 по soname. +Изменение `LD_LIBRARY_PATH` внутри процесса не решает задачу: glibc кеширует +пути поиска. Путь optional-пакета определяется через `nvidia.cublas.__path__`, +что поддерживает namespace package без `__file__`. -`_cuda_bootstrap.py` загружает `libcublas.so.12` по полному пути через -`ctypes.CDLL(path, mode=RTLD_GLOBAL)` **до** первого `import ctranslate2`. +### Windows: регистрация каталога и preload DLL -Почему не `os.environ["LD_LIBRARY_PATH"]`: на Linux/glibc динамический линкер (`ld.so`) -кеширует пути поиска при старте процесса и **не перечитывает** `LD_LIBRARY_PATH` -из environ в рамках уже запущенного процесса. +Bootstrap регистрирует `nvidia/cublas/bin` через `os.add_dll_directory`, затем +загружает по абсолютным путям сначала `cublasLt64_12.dll`, затем +`cublas64_12.dll`. Handles библиотек и регистрации каталога хранятся до +завершения процесса. Повторный вызов не загружает их заново. Это обеспечивает +доступность уже загруженных DLL для динамического загрузчика CTranslate2 и +не требует изменения системного PATH. -Почему `RTLD_GLOBAL`: без этого флага символы cublas не видны другим `.so`, -загруженным позже (в т.ч. `libctranslate2.so`). +Bootstrap вызывается только при создании CUDA-модели, до локального импорта +`WhisperModel`. Если CTranslate2 ранее импортирован для декодирования аудио, +preload всё равно выполняется до создания CUDA-модели. CPU-путь bootstrap не +вызывает. Отсутствие optional-пакета безопасно: системная cuBLAS всё ещё может +обслужить явно выбранную CUDA. Ошибка загрузки найденной Windows DLL сохраняется. -Динамический линкер кеширует загруженные библиотеки по soname — когда ctranslate2 -потом вызовет `dlopen("libcublas.so.12")`, линкер вернёт уже загруженный handle. +### Ошибки и явный выбор -### strict_device для явного --device +Явный `device` из CLI или TOML передаёт `strict_device=True` при загрузке и +транскрипции. CUDA не заменяется молча на CPU. Подсказки различают отсутствие +библиотек, проблемы драйвера, несовместимость GPU/runtime и нехватку памяти; +отказ по неподдерживаемому типу вычислений учитывает устройство, чтобы +одинаковая ошибка CTranslate2 на CPU не получала CUDA-подсказку. Исходное +сообщение сохраняется. Неизвестная ошибка не получает произвольный +совет установить Toolkit. Это одинаково для одного файла и батча. -`--device cuda` / `--device cpu` → `strict_device=True` → CUDA-ошибка = raise, без fallback. -`--device auto` → `strict_device=False` → текущее поведение с fallback на CPU. +Прежний механизм fallback оркестратора при `strict_device=False` сохранён +для существующих программных callers. Автоматический CLI-путь теперь ONNX, +поэтому он не попадает в CUDA fallback. -Мотивация: silent fallback для часового файла = 60 минут вместо 5. +## Проверка зависимостей Windows -## Последствия и tradeoffs +Статическая проверка 2026-09-21: скачаны wheels из PyPI, проверены SHA-256, +для DLL/PYD прочитаны PE imports через `objdump -p` и строки динамической +загрузки. Модели распознавания не скачивались. -**~400 MB на CPU-only Linux x86_64**: nvidia-cublas-cu12 ставится на все Linux x86_64, -включая машины без GPU. Bootstrap при этом preload'ит libcublas (overhead ~1 ms), -но она не используется, т.к. ctranslate2 не получит запрос на CUDA device. +- `ctranslate2-4.8.1-cp313-cp313-win_amd64.whl`, SHA-256 + `d52499f05a60a791aeadee28d609efa130142f376d1ea76b2b1c593bb01f8827`. + `ctranslate2.dll` содержит имена `cublas64_12.dll` и `nvcuda.dll`; + ссылок на cuDNN и динамическую cudart в этой DLL не обнаружено. + В wheel есть `cudnn64_9.dll`, но её наличие не означает использование + cuDNN проверенной рабочей DLL. OpenMP DLL поставляется внутри wheel; + импорты MSVCP/VCRUNTIME требуют Visual C++ Runtime. +- `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. В строках cuBLAS Lt также найдена + динамическая ссылка на `nvrtc64_120_0.dll`, которой нет в PE imports. + Обязательность NVRTC для Whisper этим статическим анализом не установлена; + первый реальный Windows-прогон должен проверить и этот путь загрузки. + `nvblas64_12.dll` не требуется проверенному пути CTranslate2. -Альтернатива — `[project.optional-dependencies]` + `uv sync --extra cuda`, -но тогда теряется zero-config UX. Для v1 оставляем как обязательную зависимость. +Основания: [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), +[загрузчик CTranslate2](https://github.com/OpenNMT/CTranslate2/blob/v4.8.1/python/ctranslate2/__init__.py), +[метаданные cuBLAS 12.9.2.10](https://pypi.org/pypi/nvidia-cublas-cu12/12.9.2.10/json). -**Windows GPU**: nvidia-cublas-cu12 недоступен как pip-пакет для Windows. -Единственный путь — системный CUDA toolkit (`choco install cuda` / `winget install -e --id Nvidia.CUDA`). -CLI выводит эту подсказку при CUDA-ошибке на `sys.platform == "win32"`. +Это обоснование состава extra для конкретных wheels, а не подтверждение +транскрипции на Windows GPU. Реальный прогон Windows x64 с драйвером и extra, +без Toolkit и ручной правки PATH, **не выполнен**. Linux-тесты с подменой +Windows API не заменяют такую проверку. -**Namespace package**: `nvidia.cublas` — namespace package (`__file__` is `None`), -для определения директории используется `__path__[0]`, а не `__file__`. +## Проверка поставки и поведения -## Отклонённые альтернативы +На Linux выполнены `uv run pytest`: 524 passed, 2 skipped. Пропуски — +Windows-only кодировка OEM и интеграционный bootstrap без optional-пакета. +До удаления cuBLAS интеграционный тест Linux bootstrap проходил. +Обычный `uv sync --locked` удаляет прежнюю обязательную cuBLAS; +`uv sync --extra cuda --locked --dry-run` возвращает её в план установки. -| Альтернатива | Почему отклонена | -|---|---| -| `nvidia-cudnn-cu12` в зависимостях | ctranslate2 не использует cuDNN — ноль символов, проверено через `nm -D` | -| Self-reexec с `LD_LIBRARY_PATH` | ctypes.CDLL решает задачу без перезапуска процесса; self-reexec создаёт проблемы с сигналами, tty, fd | -| `os.environ["LD_LIBRARY_PATH"] += ...` | glibc кеширует пути при старте, не перечитывает environ | -| `DeviceResolution` dataclass + `resolve_device()` | Один `bool strict_device` решает ту же задачу проще | -| ctranslate2 preflight (`get_supported_compute_types`) | Латентность; try/catch при загрузке модели не хуже | -| Bootstrap console_scripts entrypoint | Не нужен — достаточно вызова в начале `transcriber.py` | +Метаданные собранного wheel проверены для 12 сочетаний платформы, архитектуры +и extra. Полное разрешение зависимостей wheel через `uv pip compile` для Linux +и Windows, с extra и без, подтверждает отсутствие NVIDIA-пакетов в обычной +установке и наличие только cuBLAS 12.9.1.4 при выборе extra. + +Формы `uv tool install` для клона, клона с extra и именованной Git-зависимости +с extra проверены в отдельном временном каталоге tools. Для проверки самой +команды зависимости были исключены; их разрешение проверено отдельно выше. +Git-транспорт проверялся через локальный репозиторий и ветку реализации, +а не через ещё не опубликованную ветку на сервере. Версия uv — 0.12.17. + +## Последствия и альтернативы + +- Обычная установка не требует NVIDIA-пакетов. FasterWhisper CPU сохраняется. +- Дополнительные библиотеки скачиваются только по выбору пользователя. +- Единый extra заменяет требование установить полный Toolkit на Windows; + существующая системная установка библиотек остаётся допустимой. +- Отклонено сохранение обязательной cuBLAS ради готовности CUDA сразу после + установки: это увеличивает CPU-установку и не решает несовместимость GPU. +- Отклонено автоматическое включение CUDA по драйверу или extra: установка + библиотек не должна менять движок и модель по умолчанию. +- Перезапуск процесса с `LD_LIBRARY_PATH` не нужен; явная загрузка библиотек + решает проблему без изменения обработки сигналов и файловых дескрипторов. +- CUDA extra на Windows ARM64, Linux ARM и macOS в эту задачу не входит. diff --git a/docs/adr/003-pluggable-backends.md b/docs/adr/003-pluggable-backends.md index 7c99330..be7f72e 100644 --- a/docs/adr/003-pluggable-backends.md +++ b/docs/adr/003-pluggable-backends.md @@ -2,7 +2,7 @@ **Статус**: Принято **Дата**: 2026-03-21 -**Обновлено**: 2026-08-12 +**Обновлено**: 2026-09-21 ## Контекст @@ -32,8 +32,9 @@ Protocol вместо ABC — бэкенды не наследуются, дос ### Ленивые импорты Бэкенды импортируются только при выборе — `get_backend(device)` делает import внутри. -Импорт faster-whisper запускает CUDA bootstrap (~1ms), импорт openvino-genai загружает ~50MB -shared libraries. Ни то, ни другое не должно происходить, если бэкенд не выбран. +CUDA bootstrap вызывается только при создании CUDA-модели (см. ADR-001). +Импорт openvino-genai загружает ~50MB shared libraries и не должен происходить, +если бэкенд не выбран. ### Device как селектор бэкенда @@ -41,7 +42,7 @@ shared libraries. Ни то, ни другое не должно происхо - `cuda`, `cpu` → FasterWhisperBackend - `openvino` → OpenVINOBackend - `onnx` → OnnxAsrBackend -- `auto` → CUDA при наличии `nvidia-smi`, иначе ONNX на CPU +- `auto` → ONNX на CPU независимо от `nvidia-smi` и установки extra ### load_model() — единственный владелец pipeline @@ -54,7 +55,7 @@ CLI не вызывает ensure_model_available отдельно — это у Fallback живёт в `transcriber.py` (оркестратор), не в бэкендах: - CUDA ошибка → CPU (FasterWhisper) - OpenVINO ошибка → CPU (FasterWhisper) -- `strict_device=True` (явный `--device`) → ошибка без fallback +- `strict_device=True` (явный device из CLI или TOML) → ошибка без fallback При fallback в батч-режиме обновляются model, backend, model_path и actual_device через TranscribeFileResult — следующий файл использует правильный бэкенд. @@ -76,8 +77,8 @@ OpenVINO модели предквантизированы (int8/fp16), compute_ ### Зависимости бэкендов по умолчанию faster-whisper, onnx-asr/onnxruntime и openvino-genai ставятся вместе. Модели -скачиваются только для активного бэкенда. CUDA (`nvidia-cublas-cu12`) остаётся -conditional для Linux x86_64. OpenVINO — conditional для x86_64/AMD64, кроме +скачиваются только для активного бэкенда. cuBLAS подключается через extra `cuda` +для Linux/WSL x86_64 и Windows x64 (см. ADR-001). OpenVINO — conditional для x86_64/AMD64, кроме macOS; ONNX обеспечивает автоматический CPU-путь на остальных платформах. ## Последствия diff --git a/docs/adr/006-onnx-asr-backend.md b/docs/adr/006-onnx-asr-backend.md index 33a3cbf..6d9d5b9 100644 --- a/docs/adr/006-onnx-asr-backend.md +++ b/docs/adr/006-onnx-asr-backend.md @@ -102,10 +102,12 @@ Mm-hmm-редукция и иностранные вставки **не обна ## Решение **Принять onnx-asr как CPU-бэкенд автоматического профиля. -GigaAM v3 E2E RNN-T — модель по умолчанию на машинах без CUDA.** +GigaAM v3 E2E RNN-T — модель автоматического профиля на всех машинах.** -Порядок `--device auto`: CUDA при наличии `nvidia-smi`, иначе ONNX. OpenVINO и -FasterWhisper CPU остаются доступными через явный CLI-аргумент или конфиг. +С 2026-09-21 `--device auto` всегда выбирает ONNX CPU; для ASR и VAD явно +передаётся `CPUExecutionProvider`, независимо от доступных providers. CUDA, +OpenVINO и FasterWhisper CPU доступны через явный CLI-аргумент или конфиг. +Подключение CUDA описано в [ADR-001](001-cuda-bootstrap.md). GigaAM понимает только русскую речь, поэтому для остальных языков автоматический профиль не годится — нужен явный Whisper. Несовместимый язык работу не diff --git a/docs/gpu.md b/docs/gpu.md index 8b7f898..9ad3110 100644 --- a/docs/gpu.md +++ b/docs/gpu.md @@ -2,7 +2,7 @@ ## Режимы `--device` -- `auto` (по умолчанию) — CUDA при наличии `nvidia-smi`, иначе ONNX на CPU +- `auto` (по умолчанию) — всегда ONNX на CPU - `cuda` — строго NVIDIA GPU, ошибка если недоступен - `openvino` — авто-выбор OpenVINO GPU или CPU - `openvino-gpu` — строго Intel GPU через OpenVINO @@ -13,8 +13,8 @@ | Оборудование | Рекомендуемый `--device` | Бэкенд | Ожидаемая скорость | |---|---|---|---| -| NVIDIA GPU (6+ GB VRAM) | `auto` / `cuda` | faster-whisper (CTranslate2) | 7-19x реалтайм | -| Любой CPU без NVIDIA | `auto` / `onnx` | ONNX GigaAM RNN-T | 10-14x реалтайм* | +| NVIDIA GPU (6+ GB VRAM) | `cuda` + extra | faster-whisper (CTranslate2) | 7-19x реалтайм | +| Любой CPU | `auto` / `onnx` | ONNX GigaAM RNN-T | 10-14x реалтайм* | | Intel Arc iGPU / dGPU | `openvino-gpu` | OpenVINO GenAI (GPU) | TBD | | Intel/AMD x86 CPU | `openvino-cpu` | OpenVINO GenAI (CPU) | 3-10x реалтайм* | | Любой CPU, FasterWhisper | `cpu` | faster-whisper (CTranslate2) | ~1.5x реалтайм | @@ -22,7 +22,7 @@ \* По результатам контрольных прогонов на Intel и AMD CPU. Реальная скорость зависит от CPU, модели и записи. -Автоматический профиль без NVIDIA рассчитан на русскую речь: GigaAM других +Автоматический профиль рассчитан на русскую речь: GigaAM других языков не понимает. Для них берите Whisper — `openvino-cpu` на x86 или `cpu` на любой платформе. @@ -211,22 +211,16 @@ SMT/Hyper-Threading не помогает — 16 потоков на 8-ядер ## Настройка по платформам -### Linux / WSL2 (x86_64) +Для Linux/WSL x86_64 и Windows x64 используется один extra `cuda` и явный +выбор `--device cuda` либо `device = "cuda"` в TOML. Команды установки из Git, +клона и для разработки приведены в [README](../README.md#подключение-cuda). +Обычный запуск всегда использует ONNX CPU, независимо от драйвера и extra. -Библиотека cuBLAS ставится автоматически (зависимость `nvidia-cublas-cu12` подтягивается при установке). -Дополнительных шагов не требуется. - -> На ARM (aarch64) cuBLAS через pip недоступен — нужен системный CUDA toolkit. - -### Windows - -Нужен системный **CUDA 12** (ctranslate2 4.7 не совместим с CUDA 11 и 13): - -```bash -winget install -e --id Nvidia.CUDA --version 12.9 # требует запуска от имени администратора -``` - -После установки перезапустите терминал. +Драйвер NVIDIA устанавливается отдельно. Полный системный CUDA Toolkit не +нужен для пути через extra; на Windows требуется Visual C++ Runtime x64. +Состав Windows DLL проверен статически; реальная транскрипция без Toolkit +пока не проверена. Версии и границы проверки — [ADR-001](adr/001-cuda-bootstrap.md). +CUDA extra для ARM в этой задаче не поддерживается. ## Совместимость GPU @@ -331,14 +325,19 @@ threshold не помогла — проблема inherent для модели Убедитесь, что `nvidia-smi` возвращает информацию о GPU. Драйвер NVIDIA предоставляет `libcuda.so.1`, без которого CUDA не работает — его нельзя поставить через pip. -### Windows: ошибка при загрузке модели на GPU +### Отсутствующие библиотеки и несовместимый GPU -GPU на Windows требует **CUDA 12** (ctranslate2 4.7 не совместим с CUDA 11 и 13). Установите: -```bash -winget install -e --id Nvidia.CUDA --version 12.9 # требует запуска от имени администратора -``` -После установки перезапустите терминал. +При отсутствии cuBLAS подключите extra в том окружении, откуда запускается +программа, по [инструкции README](../README.md#подключение-cuda). +На Windows ошибка загрузки DLL может также означать отсутствие Visual C++ Runtime. + +`no kernel image` / `invalid device function` означают несовместимость GPU +с выбранной сборкой runtime. Установка extra её не исправит. Ошибки драйвера +требуют проверки драйвера; нехватка VRAM — меньшей модели или явного выбора CPU. +Исходная ошибка сохраняется. CLI/TOML с явной CUDA не переходят молча на CPU +ни при загрузке, ни при распознавании. ### ARM (aarch64) -cuBLAS через pip недоступен на ARM — нужен системный CUDA toolkit. +CUDA extra не поддерживается проектом на этой архитектуре. Наличие отдельных +NVIDIA wheels для ARM само по себе не подтверждает рабочий путь транскрипции. diff --git a/docs/specs/2026-09-21-transcription-execution.md b/docs/specs/2026-09-21-transcription-execution.md index 2779daa..cbf26af 100644 --- a/docs/specs/2026-09-21-transcription-execution.md +++ b/docs/specs/2026-09-21-transcription-execution.md @@ -1,6 +1,8 @@ # Владение выполнением транскрипции -**Статус:** согласовано для постановки задач. Реализация не начата. +**Статус:** согласовано. Правила выбора исполнения и optional CUDA реализованы +в #2; рефакторинг владения выполнением не начат. Windows GPU-прогон без Toolkit +остаётся невыполненным (см. ADR-001). ## Зачем @@ -14,11 +16,11 @@ отдельно. Это увеличивает interface: caller должен знать порядок вызовов и поддерживать согласованность значений. -`device` одновременно обозначает движок и аппаратное исполнение. Значение -`onnx` выбирает adapter, но не закрепляет CPU: установленный `onnx-asr 0.12.0` -по умолчанию использует доступные execution providers. При этом CLI безусловно -пишет `ONNX (CPU)`. Параметр `cpu_threads` принимается ONNX adapter, но не -используется при загрузке ASR и VAD. +`device` одновременно обозначает движок и аппаратное исполнение. До #2 значение +`onnx` выбирало adapter, но не закрепляло CPU: `onnx-asr` по умолчанию использует +доступные execution providers, хотя CLI писал `ONNX (CPU)`. В #2 для ASR и VAD +закреплён CPU provider. Параметр `cpu_threads` всё ещё принимается ONNX adapter, +но не используется при загрузке ASR и VAD. ## Согласованные ограничения @@ -72,7 +74,9 @@ model/backend/path/device. Locality — изменения загрузки и ## Выбор исполнения и ошибки Ручное включение CUDA состоит из двух явных действий: установки зависимостей -и выбора CUDA через CLI либо TOML. CUDA не участвует +и выбора CUDA через CLI либо TOML. Extra `cuda` поддерживает Linux/WSL x86_64 +и Windows x64 без обязательного системного Toolkit; совместимый драйвер +устанавливается отдельно. CUDA не участвует в `auto`, даже если найден `nvidia-smi` или установлены её библиотеки. Так установка драйвера не меняет движок и модель по умолчанию. @@ -121,7 +125,8 @@ Mock-тесты не подтверждают ускорение или совм ## Документация и прежние решения README должен различать обычную установку и подключение CUDA: дополнительные -зависимости Linux/WSL, требования Windows, явный запуск, настройку TOML +зависимости через один extra на Linux/WSL x86_64 и Windows x64, внешние +требования драйвера и Visual C++ Runtime, явный запуск, настройку TOML и диагностику отсутствующих библиотек или несовместимого железа. Команды установки из клона и через `uv tool` проверяются при реализации. CUDA extra оформляется отдельным изменением, чтобы поставку можно было проверить diff --git a/pyproject.toml b/pyproject.toml index 6cf9279..8524638 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,16 +8,23 @@ dependencies = [ "typer>=0.24.1,<1", "rich>=14.3.3,<15", "faster-whisper>=1.2.1,<2", + # CUDA-зависимости Windows проверены для этой сборки; см. ADR-001. + "ctranslate2==4.8.1", "httpx>=0.28,<1", "socksio>=1.0.0,<2", # При обновлении сверить блок tool.uv.dependency-metadata ниже. "sherpa-onnx==1.13.6", - "nvidia-cublas-cu12>=12.4,<13; sys_platform == 'linux' and platform_machine == 'x86_64'", "openvino-genai>=2026.3.0.0,<2026.4; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')", "onnx-asr[cpu,hub]>=0.12,<0.13", "onnxruntime>=1.28,<2", ] +[project.optional-dependencies] +cuda = [ + # В 12.9.2 добавилась NVRTC: обновлять после проверки DLL и bootstrap. + "nvidia-cublas-cu12==12.9.1.4; (sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and (platform_machine == 'AMD64' or platform_machine == 'x86_64'))", +] + [project.scripts] transcribe = "local_transcriber.cli:app" diff --git a/src/local_transcriber/_cuda_bootstrap.py b/src/local_transcriber/_cuda_bootstrap.py index 4914252..bed21bd 100644 --- a/src/local_transcriber/_cuda_bootstrap.py +++ b/src/local_transcriber/_cuda_bootstrap.py @@ -1,12 +1,14 @@ """ -Preload CUDA-библиотек из pip-пакетов до импорта ctranslate2. +Подготовка CUDA-библиотек из extra перед созданием CUDA-модели. Проблема: ctranslate2 на Linux делает dlopen("libcublas.so.12"), но не знает, что библиотека лежит внутри pip-пакета nvidia-cublas-cu12. -На Windows ctranslate2 решает это сам через os.add_dll_directory. +На Windows ctranslate2 не добавляет nvidia/cublas/bin в поиск DLL. +Регистрируем каталог и загружаем cuBLAS Lt, затем cuBLAS по полному пути. +Храним DLL и регистрацию каталога до завершения процесса. Решение: загружаем libcublas.so.12 по полному пути через ctypes.CDLL -с флагом RTLD_GLOBAL до первого import ctranslate2. Динамический линкер +с флагом RTLD_GLOBAL до создания CUDA-модели. Динамический линкер кеширует загруженные библиотеки по soname — когда ctranslate2 потом вызовет dlopen("libcublas.so.12"), линкер вернёт уже загруженный handle. @@ -21,20 +23,24 @@ import glob import os import sys +# Закрытие регистрации удаляет каталог из поиска DLL, поэтому храним handles. +_dll_directories: dict[str, object] = {} +_dll_libraries: dict[str, object] = {} + def ensure_cublas_loadable() -> None: - """Загружает libcublas из nvidia-cublas-cu12 в адресное пространство процесса. + """Загружает cuBLAS из nvidia-cublas-cu12 в адресное пространство процесса. - Вызывать ДО первого import ctranslate2. + Вызывать перед созданием CUDA-модели; CPU-путь не требует bootstrap. Безопасно вызывать многократно и на платформах без nvidia-cublas-cu12. """ - if sys.platform != "linux": + if sys.platform not in ("linux", "win32"): return try: import nvidia.cublas # type: ignore[import-untyped] except ImportError: - # nvidia-cublas-cu12 не установлен (Windows, macOS, или CPU-only setup) + # Optional-пакет не установлен; системная cuBLAS всё ещё может работать. return # nvidia.cublas может быть namespace package (__file__ == None), @@ -42,6 +48,21 @@ def ensure_cublas_loadable() -> None: cublas_paths = getattr(nvidia.cublas, "__path__", None) if not cublas_paths: return + if sys.platform == "win32": + for package_path in cublas_paths: + bin_dir = os.path.join(package_path, "bin") + if not os.path.isdir(bin_dir): + continue + if bin_dir not in _dll_directories: + _dll_directories[bin_dir] = os.add_dll_directory(bin_dir) + # cuBLAS зависит от Lt; явный preload также обслуживает LoadLibrary + # внутри CTranslate2. Порядок здесь существенен. + for filename in ("cublasLt64_12.dll", "cublas64_12.dll"): + dll_path = os.path.join(bin_dir, filename) + if dll_path not in _dll_libraries: + _dll_libraries[dll_path] = ctypes.WinDLL(dll_path) + return + return cublas_lib_dir = os.path.join(cublas_paths[0], "lib") if not os.path.isdir(cublas_lib_dir): return diff --git a/src/local_transcriber/backends/faster_whisper.py b/src/local_transcriber/backends/faster_whisper.py index 35ff2d1..deddc81 100644 --- a/src/local_transcriber/backends/faster_whisper.py +++ b/src/local_transcriber/backends/faster_whisper.py @@ -6,16 +6,11 @@ from collections.abc import Callable from pathlib import Path from typing import Any -# CUDA bootstrap — должен быть ДО импорта faster_whisper / ctranslate2 +from huggingface_hub import snapshot_download +from huggingface_hub.errors import LocalEntryNotFoundError + from local_transcriber._cuda_bootstrap import ensure_cublas_loadable - -ensure_cublas_loadable() - -from faster_whisper import WhisperModel # noqa: E402 -from huggingface_hub import snapshot_download # noqa: E402 -from huggingface_hub.errors import LocalEntryNotFoundError # noqa: E402 - -from local_transcriber.types import ( # noqa: E402 +from local_transcriber.types import ( Segment, TranscribeResult, Word, @@ -94,6 +89,11 @@ class FasterWhisperBackend: cpu_threads: число потоков для CPU inference (0 = дефолт библиотеки, обычно 4). """ + if device == "cuda": + ensure_cublas_loadable() + + from faster_whisper import WhisperModel + try: return WhisperModel( model_path, diff --git a/src/local_transcriber/backends/onnx_asr.py b/src/local_transcriber/backends/onnx_asr.py index 498bfb4..5f4a668 100644 --- a/src/local_transcriber/backends/onnx_asr.py +++ b/src/local_transcriber/backends/onnx_asr.py @@ -200,8 +200,9 @@ class OnnxAsrBackend: model = onnx_asr.load_model( model=model_path, quantization=quantization, + providers=["CPUExecutionProvider"], ) - vad = onnx_asr.load_vad("silero") + vad = onnx_asr.load_vad("silero", providers=["CPUExecutionProvider"]) self._vad = vad return model.with_vad(vad).with_timestamps() @@ -278,7 +279,7 @@ class OnnxAsrBackend: fallback = _whisper_fallback_model(model_name) raise ValueError( f"Модель '{model_name}' относится к Whisper и не поддерживается " - "ONNX-бэкендом. Без CUDA --device auto выбирает ONNX; " + "ONNX-бэкендом. --device auto всегда выбирает ONNX CPU; " f"укажите --device openvino-cpu --model {model_name} на x86, " f"--device cpu --model {fallback} на любой платформе " f"или --device cuda --model {fallback} при NVIDIA GPU." diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index 5a6a04a..eed8f4d 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -41,8 +41,8 @@ from .speaker_diarizer import SpeakerDiarizer, load_speaker_diarizer from .transcriber import ( Segment, TranscribeResult, - _is_cuda_error, _transcribe_file, + cuda_error_hint, load_model, ) from .types import ( @@ -65,6 +65,13 @@ app = typer.Typer() console = Console(stderr=True) +def _print_cuda_hint(exc: BaseException, device: str | None = None) -> None: + """Показывает подсказку, не заменяя исходную ошибку.""" + hint = cuda_error_hint(exc, device=device) + if hint: + console.print(hint, style="yellow", markup=False) + + def _show_cli_warning( message: Warning | str, *_args: object, @@ -341,6 +348,7 @@ def main( ) raise SystemExit(2) + resolved_device: str | None = None try: config = load_config() cli_values: CliValues = { @@ -398,21 +406,15 @@ def main( except SystemExit: raise except ValueError as 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, resolved_device) console.print(f"Ошибка: {exc}", style="red bold") raise SystemExit(1) except Exception as exc: - if _is_cuda_error(exc) and sys.platform == "win32": - console.print( - "GPU на Windows требует CUDA toolkit (включает cuBLAS).\n" - "Установите одним из способов:\n" - " choco install cuda\n" - " winget install -e --id Nvidia.CUDA\n" - "После установки перезапустите терминал.", - style="yellow", - ) + _print_cuda_hint(exc, resolved_device) if verbose: console.print_exception() else: @@ -758,6 +760,7 @@ def _run_batch( except KeyboardInterrupt: raise except Exception as 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 1521835..a516485 100644 --- a/src/local_transcriber/transcriber.py +++ b/src/local_transcriber/transcriber.py @@ -190,11 +190,60 @@ def ensure_model_available( def _is_cuda_error(exc: BaseException) -> bool: - """Проверка CUDA ошибок — используется в cli.py для Windows-диагностики.""" + """Проверяет, относится ли ошибка к CUDA-бэкенду.""" msg = str(exc).lower() return any(k in msg for k in ("cuda", "cublas", "cudnn", "out of memory")) +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", + )): + return ( + "GPU несовместим с выбранным CUDA runtime. Установка extra не исправит " + "аппаратную несовместимость. Используйте --device onnx или --device cpu." + ) + if any(k in msg for k in ( + "driver version is insufficient", "no cuda-capable device", + "cuda error: no device", "cuda_error_no_device", "libcuda.so", "nvcuda.dll", + )): + return ( + "CUDA не видит совместимого GPU/драйвера. Проверьте драйвер NVIDIA " + "и совместимость устройства с runtime. Extra не устанавливает драйвер." + ) + if any(k in msg for k in ("cublas", "cudnn", "cudart")) and any( + k in msg for k in ( + "not found", "cannot be loaded", "could not load", "could not find", + "cannot open shared object", "no such file", "winerror 126", + ) + ): + return ( + "Не найдены CUDA-библиотеки или их зависимости. Подключите extra cuda " + "в окружении приложения:\n" + " Из клона: uv tool install --force '.[cuda]'\n" + " Для разработки: uv sync --extra cuda\n" + "При установке из Git используйте инструкцию CUDA в README. " + "На Windows также нужен Visual C++ Runtime x64." + ) + if "out of memory" in msg and any(k in msg for k in ("cuda", "cublas", "cudnn")): + return ( + "Недостаточно памяти для выбранного CUDA-пути. Выберите меньшую модель " + "или явно используйте --device onnx / --device cpu." + ) + return None + + def _is_backend_error(exc: BaseException, device: str) -> bool: """Определяет, связана ли ошибка с конкретным бэкендом (а не с пользовательскими данными).""" if isinstance(exc, WordTimestampsUnavailableError): diff --git a/src/local_transcriber/utils.py b/src/local_transcriber/utils.py index 8cfafe0..84e8a22 100644 --- a/src/local_transcriber/utils.py +++ b/src/local_transcriber/utils.py @@ -2,7 +2,6 @@ import glob import platform -import shutil import subprocess import warnings from pathlib import Path @@ -16,15 +15,13 @@ SUPPORTED_EXTENSIONS = { def detect_device(requested: str = "auto") -> str: """Определяет устройство для вычислений. - При ``requested="auto"`` выбирает CUDA при наличии ``nvidia-smi``, - иначе ONNX на CPU. + При ``requested="auto"`` всегда выбирает ONNX на CPU. + CUDA включается только явным выбором пользователя. ``"openvino"`` — алиас для авто-детекта внутри OpenVINO (GPU если доступен, иначе CPU). Возвращает всегда конкретное значение (не абстрактный ``"openvino"``). """ if requested == "auto": - if shutil.which("nvidia-smi") is not None: - return "cuda" return "onnx" if requested == "openvino": if _is_openvino_gpu_available(): diff --git a/tests/test_backend_faster_whisper.py b/tests/test_backend_faster_whisper.py index 36503d8..f889c0e 100644 --- a/tests/test_backend_faster_whisper.py +++ b/tests/test_backend_faster_whisper.py @@ -7,6 +7,25 @@ from local_transcriber.backends.faster_whisper import FasterWhisperBackend from local_transcriber.types import Word +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_bootstrap_only_for_explicit_cuda(monkeypatch, device): + """CPU-модель не загружает optional CUDA-библиотеки.""" + bootstrap = MagicMock() + model_factory = MagicMock() + monkeypatch.setattr( + "local_transcriber.backends.faster_whisper.ensure_cublas_loadable", bootstrap + ) + monkeypatch.setattr("faster_whisper.WhisperModel", model_factory) + + model = FasterWhisperBackend().create_model("/model", device, "float32") + + assert model is model_factory.return_value + assert bootstrap.call_count == (1 if device == "cuda" else 0) + model_factory.assert_called_once_with( + "/model", device=device, compute_type="float32", cpu_threads=0 + ) + + def test_transcribe_returns_canonical_words(tmp_path): audio = tmp_path / "audio.wav" raw_word = SimpleNamespace(start=0.2, end=0.7, word=" Привет") diff --git a/tests/test_cli.py b/tests/test_cli.py index add639e..72627d8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -688,7 +688,7 @@ def test_cli_load_model_called_with_model_name(tmp_path): def test_cli_windows_cuda_diagnostic(tmp_path): - """CUDA error on Windows prints choco/winget install hint.""" + """При проблеме драйвера Windows не предлагает установку Toolkit.""" audio = tmp_path / "test.mp3" audio.write_bytes(b"fake") model = _make_model() @@ -712,8 +712,10 @@ def test_cli_windows_cuda_diagnostic(tmp_path): out = runner.invoke(app, [str(audio), "--device", "cuda"]) assert out.exit_code == 1 - assert "choco install cuda" in out.output - assert "winget install" in out.output + assert "драйвер" in out.output + assert "CUDA error: no device" in out.output + assert "winget install" not in out.output + assert "uv sync --extra" not in out.output def test_cli_linux_cuda_error_no_windows_hint(tmp_path): @@ -744,6 +746,115 @@ def test_cli_linux_cuda_error_no_windows_hint(tmp_path): assert "choco install cuda" not in out.output +@pytest.mark.parametrize("from_config", [False, True]) +@pytest.mark.parametrize("phase", ["load", "single", "batch"]) +@pytest.mark.parametrize( + ("message", "hint", "install_hint"), + [ + ("Library cublas64_12.dll is not found or cannot be loaded", "Не найдены", True), + ("libcublas.so.12: cannot open shared object file", "Не найдены", True), + ("CUDA error: no kernel image is available", "GPU несовместим", False), + ("CUDA driver version is insufficient for CUDA runtime version", "драйвер", False), + ("CUDA out of memory", "Недостаточно памяти", False), + ], +) +def test_explicit_cuda_errors_preserve_cause_without_fallback( + tmp_path, monkeypatch, from_config, phase, message, hint, install_hint +): + """CLI/TOML сохраняют 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") + if from_config: + (tmp_path / ".transcriber.toml").write_text('device = "cuda"\n') + args = [str(file) for file in files] + if not from_config: + args += ["--device", "cuda"] + backend = _make_backend() + error = RuntimeError(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) as get_backend: + out = runner.invoke(app, args) + + assert out.exit_code == 1 + assert message in out.output + assert hint in out.output + assert ("uv sync --extra cuda" in out.output) is install_hint + assert "winget install" not in out.output + assert "Переключение на CPU" not in out.output + assert all(call.args[0] == "cuda" for call in get_backend.call_args_list) + + +def test_default_cli_stays_on_onnx_with_nvidia_driver(tmp_path, monkeypatch): + """Обычный запуск с nvidia-smi использует прежнюю CPU-модель ONNX.""" + audio = tmp_path / "test.wav" + audio.write_bytes(b"audio") + backend = _make_backend() + backend.actual_ov_device = None + backend.actual_compute_type = "int8" + backend.transcribe.return_value = _make_result(device_used="onnx") + with ( + patch("local_transcriber.cli.load_config", return_value={}), + 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"), + ): + 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") + + +@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): """When auto-detected device differs from actual, show fallback warning.""" audio = tmp_path / "test.mp3" diff --git a/tests/test_cuda_bootstrap.py b/tests/test_cuda_bootstrap.py index 0638c0f..4d63a94 100644 --- a/tests/test_cuda_bootstrap.py +++ b/tests/test_cuda_bootstrap.py @@ -48,12 +48,73 @@ def test_ensure_cublas_loads_library(monkeypatch, tmp_path): assert calls[0][1] == ctypes.RTLD_GLOBAL -def test_ensure_cublas_skips_non_linux(monkeypatch): - """На не-Linux платформах -- no-op.""" - monkeypatch.setattr(sys, "platform", "win32") +def test_ensure_cublas_skips_macos(monkeypatch): + """На macOS bootstrap не загружает CUDA.""" + monkeypatch.setattr(sys, "platform", "darwin") ensure_cublas_loadable() # не должно бросать исключений +def test_windows_optional_package_absent(monkeypatch): + """Windows работает без extra и не меняет поиск DLL.""" + monkeypatch.setattr(sys, "platform", "win32") + monkeypatch.setitem(sys.modules, "nvidia.cublas", None) + ensure_cublas_loadable() + + +@pytest.mark.parametrize("broken", [False, True]) +def test_windows_preloads_dlls_and_keeps_handles(monkeypatch, tmp_path, broken): + """DLL загружаются по порядку один раз, ошибки не скрываются.""" + from local_transcriber import _cuda_bootstrap as bootstrap + + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + nvidia = types.ModuleType("nvidia") + nvidia.__path__ = [] + cublas = types.ModuleType("nvidia.cublas") + cublas.__path__ = [str(tmp_path)] + nvidia.cublas = cublas + monkeypatch.setitem(sys.modules, "nvidia", nvidia) + monkeypatch.setitem(sys.modules, "nvidia.cublas", cublas) + monkeypatch.setattr(sys, "platform", "win32") + monkeypatch.setattr(bootstrap, "_dll_directories", {}) + monkeypatch.setattr(bootstrap, "_dll_libraries", {}) + calls = [] + directory_handle = object() + library_handle = object() + failure = OSError("cublas64_12.dll: dependency not found") + + def add_directory(path): + calls.append(("directory", path)) + return directory_handle + + def load_library(path): + calls.append(("load", path)) + if broken: + raise failure + return library_handle + + monkeypatch.setattr(os, "add_dll_directory", add_directory, raising=False) + monkeypatch.setattr(ctypes, "WinDLL", load_library, raising=False) + original_path = os.environ.get("PATH") + + if broken: + with pytest.raises(OSError) as caught: + ensure_cublas_loadable() + assert caught.value is failure + assert bootstrap._dll_libraries == {} + else: + ensure_cublas_loadable() + ensure_cublas_loadable() + assert calls == [ + ("directory", str(bin_dir)), + ("load", str(bin_dir / "cublasLt64_12.dll")), + ("load", str(bin_dir / "cublas64_12.dll")), + ] + assert list(bootstrap._dll_libraries.values()) == [library_handle] * 2 + assert bootstrap._dll_directories[str(bin_dir)] is directory_handle + assert os.environ.get("PATH") == original_path + + def _nvidia_cublas_installed() -> bool: """Проверяет, что pip-пакет nvidia-cublas-cu12 установлен.""" try: diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py index e6e2734..118d7b6 100644 --- a/tests/test_onnx_asr.py +++ b/tests/test_onnx_asr.py @@ -1,6 +1,7 @@ """Tests for onnx-asr backend.""" import warnings +from unittest.mock import MagicMock import pytest @@ -64,12 +65,12 @@ class TestEnsureModelAvailable: def with_timestamps(self): return self - def fake_load_model(*, model, quantization): + def fake_load_model(*, model, quantization, providers): quantizations.append(quantization) return FakeAsrAdapter() monkeypatch.setattr("onnx_asr.load_model", fake_load_model) - monkeypatch.setattr("onnx_asr.load_vad", lambda model: None) + monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kwargs: None) backend = OnnxAsrBackend(compute_type_explicit=False) model_id = backend.ensure_model_available( @@ -87,6 +88,26 @@ class TestEnsureModelAvailable: class TestCreateModel: + def test_cpu_provider_is_explicit_for_asr_and_vad(self, monkeypatch): + """Доступность CoreML/CUDA не меняет исполнение ASR и VAD.""" + monkeypatch.setattr( + "onnxruntime.get_available_providers", + lambda: ["CoreMLExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"], + ) + load_asr = MagicMock() + load_vad = MagicMock() + monkeypatch.setattr("onnx_asr.load_model", load_asr) + monkeypatch.setattr("onnx_asr.load_vad", load_vad) + + OnnxAsrBackend().create_model("gigaam-v3-e2e-rnnt", "onnx", "int8") + + load_asr.assert_called_once_with( + model="gigaam-v3-e2e-rnnt", + quantization="int8", + providers=["CPUExecutionProvider"], + ) + load_vad.assert_called_once_with("silero", providers=["CPUExecutionProvider"]) + def test_wraps_vad_model_with_timestamps(self, monkeypatch): timestamped_model = object() @@ -99,7 +120,7 @@ class TestCreateModel: return FakeVadAdapter() monkeypatch.setattr("onnx_asr.load_model", lambda **kwargs: FakeAsrAdapter()) - monkeypatch.setattr("onnx_asr.load_vad", lambda model: object()) + monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kwargs: object()) model = OnnxAsrBackend().create_model("gigaam-v3-e2e-rnnt", "onnx", "int8") @@ -127,6 +148,7 @@ class TestCreateModel: return self monkeypatch.setattr("onnx_asr.load_model", fake_load_model) + monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kwargs: None) backend = OnnxAsrBackend() backend.actual_compute_type = "int8" diff --git a/tests/test_transcriber.py b/tests/test_transcriber.py index 70d1f6d..4212e6a 100644 --- a/tests/test_transcriber.py +++ b/tests/test_transcriber.py @@ -7,6 +7,7 @@ from local_transcriber.transcriber import ( Segment, TranscribeResult, _transcribe_file, + cuda_error_hint, ensure_model_available, load_model, transcribe, @@ -16,6 +17,21 @@ from local_transcriber.types import WordTimestampsUnavailableError # === Helpers === +@pytest.mark.parametrize( + "error", + [RuntimeError("out of memory"), RuntimeError("CUDA unknown error")], +) +def test_unknown_error_does_not_suggest_cuda_installation(error): + """CPU OOM и неизвестная причина не получают совет по установке CUDA.""" + assert cuda_error_hint(error) is None + + +def test_windows_loader_error_suggests_extra(): + """OSError загрузчика DLL распознаётся без обёртки RuntimeError.""" + error = OSError("[WinError 126] Could not find module 'cublas64_12.dll'") + assert "uv sync --extra cuda" in cuda_error_hint(error) + + def _make_result( count: int = 2, language: str = "ru", diff --git a/tests/test_utils.py b/tests/test_utils.py index 0d919ea..223b9a0 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -66,17 +66,17 @@ def test_detect_device_explicit_passthrough(): assert detect_device("openvino-cpu") == "openvino-cpu" -def test_detect_device_auto_cuda_when_nvidia_smi_available(): - """При доступном nvidia-smi auto выбирает CUDA.""" +def test_detect_device_auto_onnx_when_nvidia_smi_available(): + """При доступном nvidia-smi auto остаётся на ONNX CPU.""" with patch( - "local_transcriber.utils.shutil.which", return_value="/usr/bin/nvidia-smi" + "shutil.which", return_value="/usr/bin/nvidia-smi" ): - assert detect_device("auto") == "cuda" + assert detect_device("auto") == "onnx" def test_detect_device_auto_onnx_without_cuda(): """Без CUDA auto выбирает ONNX CPU.""" - with patch("local_transcriber.utils.shutil.which", return_value=None): + with patch("shutil.which", return_value=None): assert detect_device("auto") == "onnx" diff --git a/uv.lock b/uv.lock index 1cafa88..76ebb05 100644 --- a/uv.lock +++ b/uv.lock @@ -288,9 +288,9 @@ name = "local-transcriber" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "ctranslate2" }, { name = "faster-whisper" }, { name = "httpx" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "onnx-asr", extra = ["cpu", "hub"] }, { name = "onnxruntime" }, { name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')" }, @@ -300,6 +300,11 @@ dependencies = [ { name = "typer" }, ] +[package.optional-dependencies] +cuda = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'x86_64' and sys_platform == 'win32')" }, +] + [package.dev-dependencies] dev = [ { name = "kaldi-native-fbank" }, @@ -311,9 +316,10 @@ dev = [ [package.metadata] requires-dist = [ + { name = "ctranslate2", specifier = "==4.8.1" }, { name = "faster-whisper", specifier = ">=1.2.1,<2" }, { name = "httpx", specifier = ">=0.28,<1" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=12.4,<13" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'cuda') or (platform_machine == 'AMD64' and sys_platform == 'win32' and extra == 'cuda') or (platform_machine == 'x86_64' and sys_platform == 'win32' and extra == 'cuda')", specifier = "==12.9.1.4" }, { name = "onnx-asr", extras = ["cpu", "hub"], specifier = ">=0.12,<0.13" }, { name = "onnxruntime", specifier = ">=1.28,<2" }, { name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')", specifier = ">=2026.3.0.0,<2026.4" }, @@ -322,6 +328,7 @@ requires-dist = [ { name = "socksio", specifier = ">=1.0.0,<2" }, { name = "typer", specifier = ">=0.24.1,<1" }, ] +provides-extras = ["cuda"] [package.metadata.requires-dev] dev = [ @@ -445,6 +452,7 @@ version = "12.9.1.4" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/3c/aa88abe01f3be3d1f8f787d1d33dc83e76fec05945f9a28fbb41cfb99cd5/nvidia_cublas_cu12-12.9.1.4-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:453611eb21a7c1f2c2156ed9f3a45b691deda0440ec550860290dc901af5b4c2", size = 581242350, upload-time = "2025-06-05T20:04:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/45/a1/a17fade6567c57452cfc8f967a40d1035bb9301db52f27808167fbb2be2f/nvidia_cublas_cu12-12.9.1.4-py3-none-win_amd64.whl", hash = "sha256:1e5fee10662e6e52bd71dec533fbbd4971bb70a5f24f3bc3793e5c2e9dc640bf", size = 553153899, upload-time = "2025-06-05T20:13:35.556Z" }, ] [[package]]