feat(openvino): добавлена поддержка Intel GPU через OpenVINO
- Зачем: - OpenVINO backend хардкодил "CPU", хотя Intel Arc GPU доступен и даёт ~2x ускорение. - Что: - новые device modes: --device openvino-gpu, openvino-cpu; openvino — авто-детект GPU/CPU. - detect_device() проверяет Intel GPU через OpenVINO Core API (с fail-safe). - OpenVINOBackend передаёт "GPU"/"CPU" в WhisperPipeline вместо хардкода "CPU". - подсказка "Совет: --model large-v3" при наличии GPU и модели не large-v3. - .gitattributes для нормализации line endings (eol=lf). - 150 тестов, включая GPU detection, routing, fallback, CLI device info. - README, docs/gpu.md, docs/PRD.md обновлены для Intel GPU. - Проверка: - uv run pytest (150 passed). - uv run transcribe --device openvino-gpu file.mp4 на Intel Arc 140T GPU. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
dc59d2a620
commit
617181f2dc
@@ -14,14 +14,16 @@ def get_backend(device: str, *, compute_type_explicit: bool = True) -> Backend:
|
||||
Импорты ленивые — бэкенд загружается только при запросе.
|
||||
compute_type_explicit: False если compute_type пришёл из дефолтов (влияет на fallback).
|
||||
"""
|
||||
if device == "openvino":
|
||||
if device in ("openvino", "openvino-gpu", "openvino-cpu"):
|
||||
try:
|
||||
from .openvino import OpenVINOBackend
|
||||
except ImportError:
|
||||
raise ValueError(
|
||||
"OpenVINO бэкенд недоступен. Установите: pip install openvino-genai"
|
||||
) from None
|
||||
return OpenVINOBackend(compute_type_explicit=compute_type_explicit)
|
||||
return OpenVINOBackend(
|
||||
ov_device=device, compute_type_explicit=compute_type_explicit
|
||||
)
|
||||
|
||||
# cuda, cpu и всё остальное → faster-whisper
|
||||
from .faster_whisper import FasterWhisperBackend
|
||||
|
||||
@@ -46,10 +46,18 @@ MODEL_REQUIRED_FILES = [
|
||||
class OpenVINOBackend:
|
||||
"""Бэкенд транскрипции через openvino-genai WhisperPipeline."""
|
||||
|
||||
def __init__(self, compute_type_explicit: bool = True):
|
||||
"""compute_type_explicit=False означает, что compute_type пришёл из дефолтов."""
|
||||
def __init__(
|
||||
self,
|
||||
ov_device: str = "openvino-cpu",
|
||||
compute_type_explicit: bool = True,
|
||||
):
|
||||
"""ov_device: "openvino", "openvino-gpu" или "openvino-cpu".
|
||||
compute_type_explicit=False означает, что compute_type пришёл из дефолтов.
|
||||
"""
|
||||
self._ov_device = ov_device
|
||||
self._compute_type_explicit = compute_type_explicit
|
||||
self.actual_compute_type: str | None = None
|
||||
self.actual_ov_device: str | None = None
|
||||
|
||||
def ensure_model_available(
|
||||
self,
|
||||
@@ -76,6 +84,22 @@ class OpenVINOBackend:
|
||||
_validate_model_dir(downloaded_path)
|
||||
return str(downloaded_path)
|
||||
|
||||
def _resolve_ov_device(self) -> str:
|
||||
"""Резолвит device string в OpenVINO device: ``"GPU"`` или ``"CPU"``."""
|
||||
if self._ov_device == "openvino-gpu":
|
||||
return "GPU"
|
||||
if self._ov_device == "openvino-cpu":
|
||||
return "CPU"
|
||||
# "openvino" → авто-детект
|
||||
try:
|
||||
from openvino import Core
|
||||
|
||||
if "GPU" in Core().available_devices:
|
||||
return "GPU"
|
||||
except Exception:
|
||||
pass
|
||||
return "CPU"
|
||||
|
||||
def create_model(
|
||||
self,
|
||||
model_path: str,
|
||||
@@ -85,7 +109,9 @@ class OpenVINOBackend:
|
||||
"""Создаёт WhisperPipeline."""
|
||||
import openvino_genai as ov_genai
|
||||
|
||||
return ov_genai.WhisperPipeline(model_path, "CPU")
|
||||
ov_dev = self._resolve_ov_device()
|
||||
self.actual_ov_device = ov_dev
|
||||
return ov_genai.WhisperPipeline(model_path, ov_dev)
|
||||
|
||||
def transcribe(
|
||||
self,
|
||||
|
||||
@@ -21,6 +21,7 @@ from .utils import (
|
||||
detect_device,
|
||||
expand_globs,
|
||||
get_gpu_name,
|
||||
get_intel_gpu_name,
|
||||
has_existing_transcript,
|
||||
validate_input_file,
|
||||
)
|
||||
@@ -34,7 +35,10 @@ def _format_device_info(device_used: str) -> str:
|
||||
if device_used == "cuda":
|
||||
gpu_name = get_gpu_name()
|
||||
return f"CUDA ({gpu_name or 'Unknown GPU'})"
|
||||
if device_used == "openvino":
|
||||
if device_used == "openvino-gpu":
|
||||
gpu_name = get_intel_gpu_name()
|
||||
return f"OpenVINO ({gpu_name or 'Intel GPU'})"
|
||||
if device_used in ("openvino", "openvino-cpu"):
|
||||
return "OpenVINO (CPU)"
|
||||
return "CPU"
|
||||
|
||||
@@ -51,11 +55,11 @@ def main(
|
||||
output: Path | None = typer.Option(None, "--output", "-o", help="Путь к выходному файлу"),
|
||||
device: str | None = typer.Option(
|
||||
None, "--device", "-d", show_default=False,
|
||||
help="Устройство (auto|cpu|cuda|openvino) [по умолч.: auto]"
|
||||
help="Устройство (auto|cpu|cuda|openvino|openvino-gpu|openvino-cpu) [по умолч.: auto]"
|
||||
),
|
||||
compute_type: str | None = typer.Option(
|
||||
None, "--compute-type", show_default=False,
|
||||
help="Тип вычислений [по умолч.: float16 (CUDA) / int8 (OpenVINO) / float32 (CPU)]"
|
||||
help="Тип вычислений [по умолч.: float16 (CUDA) / int8 (OpenVINO GPU/CPU) / float32 (CPU)]"
|
||||
),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
|
||||
force: bool = typer.Option(False, "--force", "-f", help="Перезаписать существующие транскрипты"),
|
||||
@@ -151,6 +155,11 @@ def _run_single(
|
||||
f"Устройство: [bold]{actual_device}[/bold] "
|
||||
f"Compute: [bold]{actual_ct}[/bold]"
|
||||
)
|
||||
if actual_device == "openvino-gpu" and defaults["model"] != "large-v3":
|
||||
console.print(
|
||||
"Совет: --model large-v3 даёт лучшее качество на GPU (~2x дольше)",
|
||||
style="dim",
|
||||
)
|
||||
|
||||
with Status("Подготавливаю запуск...", console=console) as status:
|
||||
tfr = _transcribe_file(
|
||||
@@ -249,6 +258,12 @@ def _run_batch(
|
||||
compute_type_explicit=compute_type_explicit,
|
||||
)
|
||||
|
||||
if actual_device == "openvino-gpu" and defaults["model"] != "large-v3":
|
||||
console.print(
|
||||
"Совет: --model large-v3 даёт лучшее качество на GPU (~2x дольше)",
|
||||
style="dim",
|
||||
)
|
||||
|
||||
if actual_device != resolved_device:
|
||||
if requested_device == "auto":
|
||||
console.print(
|
||||
|
||||
@@ -20,11 +20,13 @@ DEVICE_DEFAULTS: dict[str, dict[str, str]] = {
|
||||
"cuda": {"model": "medium", "compute_type": "float16"},
|
||||
"cpu": {"model": "medium", "compute_type": "float32"},
|
||||
"openvino": {"model": "medium", "compute_type": "int8"},
|
||||
"openvino-gpu": {"model": "medium", "compute_type": "int8"},
|
||||
"openvino-cpu": {"model": "medium", "compute_type": "int8"},
|
||||
}
|
||||
|
||||
# Одно место правды для допустимых ключей конфига
|
||||
_VALID_KEYS = set(HARDCODED_DEFAULTS)
|
||||
_VALID_DEVICES = {"auto", "cpu", "cuda", "openvino"}
|
||||
_VALID_DEVICES = {"auto", "cpu", "cuda", "openvino", "openvino-gpu", "openvino-cpu"}
|
||||
|
||||
|
||||
def find_config_file() -> Path | None:
|
||||
|
||||
@@ -36,6 +36,12 @@ def load_model(
|
||||
try:
|
||||
_notify_status(on_status, f"Инициализирую модель на {device}...")
|
||||
model = backend.create_model(model_path, device, compute_type)
|
||||
# Резолвим actual_device по реальному OpenVINO device
|
||||
ov_dev = getattr(backend, "actual_ov_device", None)
|
||||
if ov_dev == "GPU" and actual_device != "openvino-gpu":
|
||||
actual_device = "openvino-gpu"
|
||||
elif ov_dev == "CPU" and actual_device.startswith("openvino") and actual_device != "openvino-cpu":
|
||||
actual_device = "openvino-cpu"
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
if device != "cpu" and _is_backend_error(exc, device):
|
||||
if strict_device:
|
||||
@@ -157,7 +163,7 @@ def _is_backend_error(exc: BaseException, device: str) -> bool:
|
||||
"""Определяет, связана ли ошибка с конкретным бэкендом (а не с пользовательскими данными)."""
|
||||
if device in ("cuda", "cpu"):
|
||||
return _is_cuda_error(exc)
|
||||
if device == "openvino":
|
||||
if device.startswith("openvino"):
|
||||
return _is_openvino_error(exc)
|
||||
return False
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class TranscribeResult:
|
||||
language: str
|
||||
language_probability: float
|
||||
duration: float # seconds
|
||||
device_used: str # "cpu" / "cuda" / "openvino"
|
||||
device_used: str # "cpu" / "cuda" / "openvino-gpu" / "openvino-cpu"
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -16,16 +16,23 @@ SUPPORTED_EXTENSIONS = {
|
||||
def detect_device(requested: str = "auto") -> str:
|
||||
"""Определяет устройство для вычислений.
|
||||
|
||||
При ``requested="auto"`` проверяет: CUDA → OpenVINO → CPU.
|
||||
Явное значение возвращается как есть.
|
||||
При ``requested="auto"`` проверяет: CUDA → OpenVINO GPU → OpenVINO CPU → CPU.
|
||||
``"openvino"`` — алиас для авто-детекта внутри OpenVINO (GPU если доступен, иначе CPU).
|
||||
Возвращает всегда конкретное значение (не абстрактный ``"openvino"``).
|
||||
"""
|
||||
if requested != "auto":
|
||||
return requested
|
||||
if shutil.which("nvidia-smi") is not None:
|
||||
return "cuda"
|
||||
if _is_openvino_available():
|
||||
return "openvino"
|
||||
return "cpu"
|
||||
if requested == "auto":
|
||||
if shutil.which("nvidia-smi") is not None:
|
||||
return "cuda"
|
||||
if _is_openvino_gpu_available():
|
||||
return "openvino-gpu"
|
||||
if _is_openvino_available():
|
||||
return "openvino-cpu"
|
||||
return "cpu"
|
||||
if requested == "openvino":
|
||||
if _is_openvino_gpu_available():
|
||||
return "openvino-gpu"
|
||||
return "openvino-cpu"
|
||||
return requested
|
||||
|
||||
|
||||
def _is_openvino_available() -> bool:
|
||||
@@ -40,6 +47,32 @@ def _is_openvino_available() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _is_openvino_gpu_available() -> bool:
|
||||
"""Проверяет доступность Intel GPU через OpenVINO.
|
||||
|
||||
Сохраняет x86/AMD64 gate и глотает все ошибки импорта/runtime.
|
||||
"""
|
||||
if not _is_openvino_available():
|
||||
return False
|
||||
try:
|
||||
from openvino import Core
|
||||
|
||||
return "GPU" in Core().available_devices
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_intel_gpu_name() -> str | None:
|
||||
"""Возвращает название Intel GPU через OpenVINO (для метаданных транскрипта)."""
|
||||
try:
|
||||
from openvino import Core
|
||||
|
||||
name = Core().get_property("GPU", "FULL_DEVICE_NAME")
|
||||
return name if name else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_gpu_name() -> str | None:
|
||||
"""Возвращает название GPU через ``nvidia-smi`` (для метаданных транскрипта)."""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user