- Зачем:
- зафиксировать архитектурное решение по GPU runtime и rejected alternatives,
чтобы не переизобретать отклонённые подходы в будущем.
- Что:
- создан docs/adr/001-cuda-bootstrap.md (контекст, решение, tradeoffs, альтернативы).
- PRD 4.2: исправлено описание CUDA-зависимостей (cuDNN не нужен, cuBLAS из pip).
- plan.md: добавлен выполненный шаг 5.1 со ссылкой на ADR.
- удалены docs/plan-gpu-runtime.md и отчёты ревью (review-stages-*).
- Проверка:
- cat docs/adr/001-cuda-bootstrap.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4.9 KiB
ADR-001: Preload libcublas из pip-пакета для GPU на Linux/WSL2
Статус: Принято Дата: 2026-03-18
Контекст
ctranslate2 (backend faster-whisper) в runtime делает dlopen("libcublas.so.12"),
но не бандлит эту библиотеку в свой wheel — ожидает её в системе.
Без установленного CUDA toolkit uv run transcribe --device cuda падает с ошибкой.
Ключевые факты:
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тоже не бандлится
Решение
nvidia-cublas-cu12 как pip-зависимость
В pyproject.toml:
"nvidia-cublas-cu12>=12.4; sys_platform == 'linux' and platform_machine == 'x86_64'"
Нижняя граница >=12.4 — ctranslate2 собран с CUDA 12.4. Пакет доступен только
для Linux x86_64 (на Windows и macOS не устанавливается по platform marker).
ctypes.CDLL preload вместо LD_LIBRARY_PATH
_cuda_bootstrap.py загружает libcublas.so.12 по полному пути через
ctypes.CDLL(path, mode=RTLD_GLOBAL) до первого import ctranslate2.
Почему не os.environ["LD_LIBRARY_PATH"]: на Linux/glibc динамический линкер (ld.so)
кеширует пути поиска при старте процесса и не перечитывает LD_LIBRARY_PATH
из environ в рамках уже запущенного процесса.
Почему RTLD_GLOBAL: без этого флага символы cublas не видны другим .so,
загруженным позже (в т.ч. libctranslate2.so).
Динамический линкер кеширует загруженные библиотеки по soname — когда ctranslate2
потом вызовет dlopen("libcublas.so.12"), линкер вернёт уже загруженный handle.
strict_device для явного --device
--device cuda / --device cpu → strict_device=True → CUDA-ошибка = raise, без fallback.
--device auto → strict_device=False → текущее поведение с fallback на CPU.
Мотивация: silent fallback для часового файла = 60 минут вместо 5.
Последствия и tradeoffs
~400 MB на CPU-only Linux x86_64: nvidia-cublas-cu12 ставится на все Linux x86_64, включая машины без GPU. Bootstrap при этом preload'ит libcublas (overhead ~1 ms), но она не используется, т.к. ctranslate2 не получит запрос на CUDA device.
Альтернатива — [project.optional-dependencies] + uv sync --extra cuda,
но тогда теряется zero-config UX. Для v1 оставляем как обязательную зависимость.
Windows GPU: nvidia-cublas-cu12 недоступен как pip-пакет для Windows.
Единственный путь — системный CUDA toolkit (choco install cuda / winget install -e --id Nvidia.CUDA).
CLI выводит эту подсказку при CUDA-ошибке на sys.platform == "win32".
Namespace package: nvidia.cublas — namespace package (__file__ is None),
для определения директории используется __path__[0], а не __file__.
Отклонённые альтернативы
| Альтернатива | Почему отклонена |
|---|---|
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 |