feat(cuda): добавлен прозрачный GPU runtime для Linux/WSL2

- Зачем:
  - ctranslate2 требует libcublas.so.12 для CUDA, но не бандлит её в wheel —
    без системного CUDA toolkit GPU не работает из коробки.
- Что:
  - добавлена зависимость nvidia-cublas-cu12 (Linux x86_64).
  - создан _cuda_bootstrap.py: preload libcublas через ctypes.CDLL(RTLD_GLOBAL)
    до импорта ctranslate2 (LD_LIBRARY_PATH не работает — glibc кеширует пути).
  - добавлен strict_device в transcriber: --device cuda/cpu не делает silent fallback.
  - CLI: диагностика requested vs resolved device, Windows CUDA-подсказка.
- Проверка:
  - uv run pytest -v (52 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 18:37:14 +03:00
co-authored by Claude Opus 4.6
parent 3d14ed7b86
commit bdb9b06855
8 changed files with 399 additions and 14 deletions
+107
View File
@@ -0,0 +1,107 @@
import ctypes
import glob
import os
import sys
import types
import pytest
from local_transcriber._cuda_bootstrap import ensure_cublas_loadable, is_cublas_available
def test_ensure_cublas_no_nvidia_package(monkeypatch):
"""Без nvidia-cublas-cu12 -- ничего не падает."""
monkeypatch.setattr(sys, "platform", "linux")
monkeypatch.setitem(sys.modules, "nvidia.cublas", None)
ensure_cublas_loadable() # не должно бросать исключений
def test_ensure_cublas_loads_library(monkeypatch, tmp_path):
"""С nvidia.cublas -- вызывает ctypes.CDLL с полным путём и RTLD_GLOBAL."""
monkeypatch.setattr(sys, "platform", "linux")
# Создаём фейковый nvidia.cublas с lib/libcublas.so.12
lib_dir = tmp_path / "lib"
lib_dir.mkdir()
fake_so = lib_dir / "libcublas.so.12"
fake_so.touch()
# Мокаем родительский пакет nvidia (иначе import nvidia.cublas упадёт)
fake_nvidia = types.ModuleType("nvidia")
fake_nvidia.__path__ = [str(tmp_path)]
fake_cublas = types.ModuleType("nvidia.cublas")
fake_cublas.__path__ = [str(tmp_path)]
fake_nvidia.cublas = fake_cublas
monkeypatch.setitem(sys.modules, "nvidia", fake_nvidia)
monkeypatch.setitem(sys.modules, "nvidia.cublas", fake_cublas)
calls = []
monkeypatch.setattr(ctypes, "CDLL", lambda path, mode=0: calls.append((path, mode)))
ensure_cublas_loadable()
assert len(calls) == 1
assert calls[0][0] == str(fake_so)
assert calls[0][1] == ctypes.RTLD_GLOBAL
def test_ensure_cublas_skips_non_linux(monkeypatch):
"""На не-Linux платформах -- no-op."""
monkeypatch.setattr(sys, "platform", "win32")
ensure_cublas_loadable() # не должно бросать исключений
def _nvidia_cublas_installed() -> bool:
"""Проверяет, что pip-пакет nvidia-cublas-cu12 установлен."""
try:
import nvidia.cublas # type: ignore[import-untyped]
cublas_paths = getattr(nvidia.cublas, "__path__", None)
if not cublas_paths:
return False
lib_dir = os.path.join(cublas_paths[0], "lib")
return any(glob.glob(os.path.join(lib_dir, "libcublas.so.12*")))
except ImportError:
return False
def _system_cublas_available() -> bool:
"""Проверяет, что libcublas.so.12 доступна через системный линкер (без bootstrap)."""
try:
ctypes.CDLL("libcublas.so.12")
return True
except OSError:
return False
@pytest.mark.skipif(
sys.platform != "linux",
reason="CUDA bootstrap только для Linux",
)
@pytest.mark.skipif(
not _nvidia_cublas_installed(),
reason="nvidia-cublas-cu12 не установлен",
)
def test_bootstrap_makes_cublas_resolvable():
"""Bootstrap из pip-пакета делает libcublas.so.12 резолвимой.
Тест проходит ТОЛЬКО если:
1. nvidia-cublas-cu12 установлен (иначе skip)
2. libcublas НЕ доступна через системный линкер до bootstrap
(иначе skip -- тест не может доказать, что сработал именно bootstrap)
3. После ensure_cublas_loadable() -- libcublas доступна
"""
if _system_cublas_available():
pytest.skip(
"libcublas.so.12 уже доступна через системный линкер -- "
"невозможно проверить, что сработал именно bootstrap"
)
ensure_cublas_loadable()
assert is_cublas_available(), (
"nvidia-cublas-cu12 установлен, но после bootstrap "
"libcublas.so.12 всё ещё не резолвится через dlopen"
)