feat(cuda): добавлен optional runtime для Linux и Windows
This commit is contained in:
1 parent
937eafe8db
commit
99ed5ed92f
10 files changed
+282
-36
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) -> None:
|
||||
"""Показывает подсказку, не заменяя исходную ошибку."""
|
||||
hint = cuda_error_hint(exc)
|
||||
if hint:
|
||||
console.print(hint, style="yellow", markup=False)
|
||||
|
||||
|
||||
def _show_cli_warning(
|
||||
message: Warning | str,
|
||||
*_args: object,
|
||||
@@ -398,21 +405,15 @@ def main(
|
||||
except SystemExit:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
_print_cuda_hint(exc)
|
||||
console.print(f"Ошибка: {exc}", style="red bold")
|
||||
raise SystemExit(1)
|
||||
except (FileNotFoundError,) as exc:
|
||||
_print_cuda_hint(exc)
|
||||
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)
|
||||
if verbose:
|
||||
console.print_exception()
|
||||
else:
|
||||
@@ -758,6 +759,7 @@ def _run_batch(
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception as exc:
|
||||
_print_cuda_hint(exc)
|
||||
if verbose:
|
||||
console.print_exception()
|
||||
else:
|
||||
|
||||
@@ -190,11 +190,52 @@ 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) -> str | None:
|
||||
"""Подсказывает действие только для распознанной причины CUDA-ошибки."""
|
||||
msg = str(exc).lower()
|
||||
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):
|
||||
|
||||
Reference in new issue
Block a user