fix(whispercpp): map GPU picker to ggml devices and size threads by effective backend (#1001)
* fix(whispercpp): map GPU picker to ggml devices and size threads by effective backend The GPU picker and auto selection used vulkaninfo indices, but whisper.cpp's gpu_device is an ordinal over the GPU/IGPU devices ggml registered. ggml skips Vulkan devices it cannot use (for example a Haswell iGPU), so the indices shift and a selected NVIDIA card could fall back to CPU while logs claimed Vulkan. Read ggml's device registry through the loaded pywhispercpp extension, select and label devices by that ordinal, run on CPU explicitly when ggml has no GPU, log the effective backend after load, and pick n_threads from the effective backend instead of the presence of GPU libraries. Fixes #987 Fixes #990 * test(whispercpp): move new ggml probe tests into their test class
This commit is contained in:
@@ -137,6 +137,26 @@ def _pywhispercpp_distribution_version() -> Optional[tuple[int, ...]]:
|
||||
return tuple(parts) if parts else None
|
||||
|
||||
|
||||
def _select_ggml_gpu_device(
|
||||
devices: list[dict[str, Any]], requested_index: Optional[int]
|
||||
) -> Optional[dict[str, Any]]:
|
||||
if requested_index is not None and requested_index >= 0:
|
||||
selected = next((device for device in devices if device["index"] == requested_index), None)
|
||||
if selected is not None:
|
||||
return selected
|
||||
logger.warning(
|
||||
"Requested whisper.cpp GPU index %s is unavailable; usable ggml GPU devices: %s. "
|
||||
"Selecting automatically.",
|
||||
requested_index,
|
||||
devices,
|
||||
)
|
||||
|
||||
return next(
|
||||
(device for device in devices if device["device_type"] == "discrete"),
|
||||
devices[0] if devices else None,
|
||||
)
|
||||
|
||||
|
||||
def resolve_whisper_language(language: str) -> Optional[str]:
|
||||
"""Map a catalog language id to a Whisper / whisper.cpp language code.
|
||||
|
||||
@@ -1515,12 +1535,21 @@ class SpeechRecognitionManager:
|
||||
return compatible_kwargs
|
||||
|
||||
def _load_model_with_compatible_params(
|
||||
self, model_path: str, model_kwargs: dict, gpu_device: Optional[int] = None
|
||||
):
|
||||
self,
|
||||
model_path: str,
|
||||
model_kwargs: dict[str, Any],
|
||||
gpu_device: Optional[int] = None,
|
||||
use_gpu: Optional[bool] = None,
|
||||
) -> Any:
|
||||
from pywhispercpp.model import Model
|
||||
|
||||
compatible_kwargs = self._filter_whispercpp_model_kwargs(model_kwargs)
|
||||
context_params: dict[str, Any] = {}
|
||||
if use_gpu is not None:
|
||||
context_params["use_gpu"] = use_gpu
|
||||
if gpu_device is not None and gpu_device >= 0:
|
||||
context_params["gpu_device"] = gpu_device
|
||||
if context_params:
|
||||
dist_version = _pywhispercpp_distribution_version()
|
||||
too_old_for_context_params = dist_version is not None and dist_version < (1, 5, 0)
|
||||
try:
|
||||
@@ -1540,7 +1569,7 @@ class SpeechRecognitionManager:
|
||||
version_label,
|
||||
)
|
||||
elif supports_context_params:
|
||||
compatible_kwargs["context_params"] = {"gpu_device": gpu_device}
|
||||
compatible_kwargs["context_params"] = context_params
|
||||
else:
|
||||
logger.warning(
|
||||
"pywhispercpp does not support context_params; "
|
||||
@@ -1582,62 +1611,118 @@ class SpeechRecognitionManager:
|
||||
_prefer_discrete_vulkan_device,
|
||||
detect_compute_backend,
|
||||
detect_cpu_info,
|
||||
detect_ggml_gpu_devices,
|
||||
detect_vulkan_devices,
|
||||
get_backend_display_name,
|
||||
)
|
||||
|
||||
host_backend, host_info = detect_compute_backend()
|
||||
actual_gpu_backend = self._detect_pywhispercpp_gpu_backend()
|
||||
ggml_devices = detect_ggml_gpu_devices()
|
||||
logger.info("whisper.cpp backend selection priority: Vulkan -> CUDA -> CPU")
|
||||
|
||||
selected_gpu_device = None
|
||||
use_gpu: Optional[bool] = None
|
||||
runtime_backend = ComputeBackend.CPU
|
||||
runtime_info = detect_cpu_info()
|
||||
|
||||
if actual_gpu_backend == "cuda":
|
||||
# pywhispercpp CUDA uses CUDA device ordinals (0 = first NVIDIA GPU).
|
||||
# Vulkan enumeration on hybrid laptops lists iGPU as GPU0 and dGPU as
|
||||
# GPU1; passing that Vulkan index to CUDA selects the CPU fallback.
|
||||
vulkan_gpu_index = self.whispercpp_gpu_device
|
||||
if vulkan_gpu_index is None or vulkan_gpu_index < 0:
|
||||
vulkan_gpu_index = _prefer_discrete_vulkan_device()
|
||||
if vulkan_gpu_index not in (None, 0):
|
||||
logger.info(
|
||||
"pywhispercpp is CUDA-backed; using CUDA device 0 "
|
||||
f"(Vulkan GPU index {vulkan_gpu_index} does not map to CUDA ordinals)"
|
||||
if ggml_devices == []:
|
||||
use_gpu = False
|
||||
skipped_devices = [
|
||||
device["name"]
|
||||
for device in detect_vulkan_devices()
|
||||
if device.get("device_type") != "software"
|
||||
]
|
||||
logger.warning(
|
||||
"whisper.cpp found no usable ggml GPU device and will use CPU. "
|
||||
"Vulkan hardware devices skipped by ggml: %s",
|
||||
", ".join(skipped_devices) if skipped_devices else "none found",
|
||||
)
|
||||
else:
|
||||
logger.info("pywhispercpp is CUDA-backed; using CUDA device 0")
|
||||
selected_gpu_device = 0
|
||||
runtime_backend = ComputeBackend.CUDA
|
||||
runtime_info = host_info if host_backend == ComputeBackend.CUDA else "NVIDIA GPU"
|
||||
# CUDA device ordinals are independent of vulkaninfo indices.
|
||||
vulkan_gpu_index = self.whispercpp_gpu_device
|
||||
if vulkan_gpu_index is None or vulkan_gpu_index < 0:
|
||||
vulkan_gpu_index = _prefer_discrete_vulkan_device()
|
||||
if vulkan_gpu_index not in (None, 0):
|
||||
logger.info(
|
||||
"pywhispercpp is CUDA-backed; using CUDA device 0 "
|
||||
"(Vulkan GPU index %s does not map to CUDA ordinals)",
|
||||
vulkan_gpu_index,
|
||||
)
|
||||
else:
|
||||
logger.info("pywhispercpp is CUDA-backed; using CUDA device 0")
|
||||
selected_gpu_device = 0
|
||||
runtime_backend = ComputeBackend.CUDA
|
||||
runtime_info = host_info if host_backend == ComputeBackend.CUDA else "NVIDIA GPU"
|
||||
elif actual_gpu_backend == "vulkan":
|
||||
gpu_device_index = self.whispercpp_gpu_device
|
||||
if gpu_device_index is None or gpu_device_index < 0:
|
||||
gpu_device_index = _prefer_discrete_vulkan_device()
|
||||
|
||||
devices = detect_vulkan_devices()
|
||||
runtime_backend = ComputeBackend.VULKAN
|
||||
if gpu_device_index is not None:
|
||||
device_name = next(
|
||||
(d["name"] for d in devices if d["index"] == gpu_device_index),
|
||||
"unknown",
|
||||
)
|
||||
logger.info(f"Using Vulkan GPU [{gpu_device_index}]: {device_name}")
|
||||
selected_gpu_device = gpu_device_index
|
||||
runtime_info = device_name
|
||||
elif devices:
|
||||
runtime_info = devices[0]["name"]
|
||||
logger.warning(
|
||||
"pywhispercpp is Vulkan-backed but no preferred GPU was selected. "
|
||||
"whisper.cpp will default to GPU 0, which may be an iGPU on hybrid laptops."
|
||||
)
|
||||
if ggml_devices is not None:
|
||||
vulkan_devices = detect_vulkan_devices()
|
||||
if ggml_devices:
|
||||
selected_device = _select_ggml_gpu_device(
|
||||
ggml_devices, self.whispercpp_gpu_device
|
||||
)
|
||||
ggml_names = {device["name"] for device in ggml_devices}
|
||||
skipped_devices = [
|
||||
device["name"]
|
||||
for device in vulkan_devices
|
||||
if device.get("device_type") != "software"
|
||||
and device["name"] not in ggml_names
|
||||
]
|
||||
if skipped_devices:
|
||||
logger.info(
|
||||
"Vulkan devices not registered by ggml: %s",
|
||||
", ".join(skipped_devices),
|
||||
)
|
||||
if selected_device is not None:
|
||||
runtime_backend = ComputeBackend.VULKAN
|
||||
selected_gpu_device = selected_device["index"]
|
||||
runtime_info = selected_device["name"]
|
||||
logger.info(
|
||||
"Requesting Vulkan GPU [%s]: %s (%s)",
|
||||
selected_device["index"],
|
||||
selected_device["name"],
|
||||
selected_device["backend_name"],
|
||||
)
|
||||
else:
|
||||
use_gpu = False
|
||||
skipped_devices = [
|
||||
device["name"]
|
||||
for device in vulkan_devices
|
||||
if device.get("device_type") != "software"
|
||||
]
|
||||
logger.warning(
|
||||
"whisper.cpp found no usable Vulkan device and will use CPU. "
|
||||
"Vulkan hardware devices skipped by ggml: %s",
|
||||
", ".join(skipped_devices) if skipped_devices else "none found",
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"pywhispercpp is Vulkan-backed but vulkaninfo found no devices. "
|
||||
"Install vulkan-tools so Vocalinux can pick a discrete GPU. "
|
||||
"whisper.cpp will default to GPU 0, which may be an iGPU on hybrid laptops."
|
||||
)
|
||||
gpu_device_index = self.whispercpp_gpu_device
|
||||
if gpu_device_index is None or gpu_device_index < 0:
|
||||
gpu_device_index = _prefer_discrete_vulkan_device()
|
||||
|
||||
devices = detect_vulkan_devices()
|
||||
runtime_backend = ComputeBackend.VULKAN
|
||||
if gpu_device_index is not None:
|
||||
device_name = next(
|
||||
(d["name"] for d in devices if d["index"] == gpu_device_index),
|
||||
"unknown",
|
||||
)
|
||||
logger.info(f"Using Vulkan GPU [{gpu_device_index}]: {device_name}")
|
||||
selected_gpu_device = gpu_device_index
|
||||
runtime_info = device_name
|
||||
elif devices:
|
||||
runtime_info = devices[0]["name"]
|
||||
logger.warning(
|
||||
"pywhispercpp is Vulkan-backed but no preferred GPU was selected. "
|
||||
"whisper.cpp will default to GPU 0, which may be an iGPU on hybrid laptops."
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"pywhispercpp is Vulkan-backed but vulkaninfo found no devices. "
|
||||
"Install vulkan-tools so Vocalinux can pick a discrete GPU. "
|
||||
"whisper.cpp will default to GPU 0, which may be an iGPU on hybrid laptops."
|
||||
)
|
||||
elif host_backend != ComputeBackend.CPU:
|
||||
logger.warning(
|
||||
"Host reports %s (%s), but pywhispercpp has no GPU libraries. "
|
||||
@@ -1649,7 +1734,11 @@ class SpeechRecognitionManager:
|
||||
host_info,
|
||||
)
|
||||
|
||||
if actual_gpu_backend in ("vulkan", "cuda") and host_backend != actual_gpu_backend:
|
||||
if (
|
||||
runtime_backend == actual_gpu_backend
|
||||
and actual_gpu_backend in ("vulkan", "cuda")
|
||||
and host_backend != actual_gpu_backend
|
||||
):
|
||||
logger.info(
|
||||
"Host capability label is %s; bundled pywhispercpp libraries are %s. "
|
||||
"Using the bundled %s backend.",
|
||||
@@ -1659,7 +1748,9 @@ class SpeechRecognitionManager:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"whisper.cpp using {get_backend_display_name(runtime_backend)} backend: {runtime_info}"
|
||||
"whisper.cpp requesting %s backend: %s",
|
||||
get_backend_display_name(runtime_backend),
|
||||
runtime_info,
|
||||
)
|
||||
|
||||
import psutil
|
||||
@@ -1678,9 +1769,8 @@ class SpeechRecognitionManager:
|
||||
logger.info(f"Loading whisper.cpp '{self.model_size}' model...")
|
||||
self.model = None # Release previous model if re-initializing
|
||||
|
||||
has_gpu_libs = actual_gpu_backend in ("vulkan", "cuda")
|
||||
|
||||
if has_gpu_libs and not (
|
||||
has_gpu_backend = runtime_backend in (ComputeBackend.VULKAN, ComputeBackend.CUDA)
|
||||
if has_gpu_backend and not (
|
||||
self.whispercpp_n_threads is not None and self.whispercpp_n_threads > 0
|
||||
):
|
||||
n_threads = max(1, multiprocessing.cpu_count() // 4)
|
||||
@@ -1695,18 +1785,25 @@ class SpeechRecognitionManager:
|
||||
|
||||
try:
|
||||
self.model = self._load_model_with_compatible_params(
|
||||
model_path, model_kwargs, gpu_device=selected_gpu_device
|
||||
model_path, model_kwargs, gpu_device=selected_gpu_device, use_gpu=use_gpu
|
||||
)
|
||||
except RuntimeError as model_error:
|
||||
loaded_backend = self._handle_gpu_fallback(
|
||||
model_error, model_path, model_kwargs, ComputeBackend.CPU
|
||||
)
|
||||
runtime_info = detect_cpu_info()
|
||||
|
||||
effective_threads = self._whispercpp_effective_threads
|
||||
if effective_threads is None:
|
||||
effective_threads = n_threads
|
||||
|
||||
load_duration = time.time() - load_start_time
|
||||
logger.info(
|
||||
"whisper.cpp loaded with effective %s backend: %s (n_threads=%d)",
|
||||
get_backend_display_name(loaded_backend),
|
||||
runtime_info,
|
||||
effective_threads,
|
||||
)
|
||||
logger.info(
|
||||
f"whisper.cpp configured with n_threads={effective_threads} "
|
||||
f"({loaded_backend} backend)"
|
||||
|
||||
@@ -2422,6 +2422,24 @@ class ModelDownloadDialog(Gtk.Dialog):
|
||||
self.add_button("OK", Gtk.ResponseType.OK)
|
||||
|
||||
|
||||
def _populate_gpu_device_choices(combo: Any, saved_device: Optional[int]) -> None:
|
||||
combo.remove_all()
|
||||
combo.append("-1", "Auto (discrete GPU)")
|
||||
|
||||
from ..utils.whispercpp_model_info import detect_ggml_gpu_devices
|
||||
|
||||
devices = detect_ggml_gpu_devices()
|
||||
if devices is None:
|
||||
devices = detect_vulkan_devices()
|
||||
for device in devices:
|
||||
type_label = device["device_type"].capitalize()
|
||||
label = f"[{device['index']}] {device['name']} ({type_label})"
|
||||
combo.append(str(device["index"]), label)
|
||||
|
||||
if saved_device is None or not combo.set_active_id(str(saved_device)):
|
||||
combo.set_active_id("-1")
|
||||
|
||||
|
||||
class SettingsDialog(AboutSectionMixin):
|
||||
"""Modern GTK Dialog for configuring Vocalinux settings."""
|
||||
|
||||
@@ -9592,23 +9610,10 @@ For now, the engine has been reverted to VOSK."""
|
||||
error_dialog.destroy()
|
||||
return False
|
||||
|
||||
def _populate_gpu_devices(self):
|
||||
def _populate_gpu_devices(self) -> None:
|
||||
"""Populate the GPU device dropdown with available Vulkan devices."""
|
||||
self.gpu_device_combo.remove_all()
|
||||
self.gpu_device_combo.append("-1", "Auto (discrete GPU)")
|
||||
|
||||
devices = detect_vulkan_devices()
|
||||
for device in devices:
|
||||
type_label = device["device_type"].capitalize()
|
||||
label = f"[{device['index']}] {device['name']} ({type_label})"
|
||||
self.gpu_device_combo.append(str(device["index"]), label)
|
||||
|
||||
saved_device = self.config_manager.get("advanced", "whispercpp_gpu_device", None)
|
||||
if saved_device is None:
|
||||
self.gpu_device_combo.set_active_id("-1")
|
||||
else:
|
||||
if not self.gpu_device_combo.set_active_id(str(saved_device)):
|
||||
self.gpu_device_combo.set_active_id("-1")
|
||||
_populate_gpu_device_choices(self.gpu_device_combo, saved_device)
|
||||
|
||||
def _populate_audio_devices(self):
|
||||
"""Populate the audio device dropdown with available input devices."""
|
||||
|
||||
@@ -5,13 +5,15 @@ This module provides model metadata and hardware acceleration detection
|
||||
for whisper.cpp, supporting Vulkan, CUDA, and CPU backends.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from functools import lru_cache
|
||||
from typing import Optional, cast
|
||||
from functools import cache, lru_cache
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
from .host_process import host_env
|
||||
from .model_checksums import whispercpp_revision
|
||||
@@ -347,6 +349,61 @@ def detect_vulkan_devices() -> list[dict]:
|
||||
return devices
|
||||
|
||||
|
||||
@cache
|
||||
def detect_ggml_gpu_devices() -> Optional[list[dict[str, Any]]]:
|
||||
"""Return whisper.cpp's usable GPU devices in its device-selection order."""
|
||||
try:
|
||||
raw_version = version("pywhispercpp")
|
||||
except (ImportError, PackageNotFoundError):
|
||||
return None
|
||||
|
||||
version_match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", raw_version)
|
||||
if version_match is None:
|
||||
return None
|
||||
installed_version = tuple(int(part or 0) for part in version_match.groups())
|
||||
if installed_version < (1, 5, 0):
|
||||
return None
|
||||
|
||||
try:
|
||||
from . import pywhispercpp_loader
|
||||
|
||||
pywhispercpp_loader.preload_shared_libraries()
|
||||
import _pywhispercpp
|
||||
|
||||
library = ctypes.CDLL(_pywhispercpp.__file__)
|
||||
library.ggml_backend_dev_count.argtypes = []
|
||||
library.ggml_backend_dev_count.restype = ctypes.c_size_t
|
||||
library.ggml_backend_dev_get.argtypes = [ctypes.c_size_t]
|
||||
library.ggml_backend_dev_get.restype = ctypes.c_void_p
|
||||
library.ggml_backend_dev_name.argtypes = [ctypes.c_void_p]
|
||||
library.ggml_backend_dev_name.restype = ctypes.c_char_p
|
||||
library.ggml_backend_dev_description.argtypes = [ctypes.c_void_p]
|
||||
library.ggml_backend_dev_description.restype = ctypes.c_char_p
|
||||
library.ggml_backend_dev_type.argtypes = [ctypes.c_void_p]
|
||||
library.ggml_backend_dev_type.restype = ctypes.c_int
|
||||
|
||||
devices: list[dict[str, Any]] = []
|
||||
for registry_index in range(library.ggml_backend_dev_count()):
|
||||
device = library.ggml_backend_dev_get(registry_index)
|
||||
device_type = library.ggml_backend_dev_type(device)
|
||||
if device_type not in (1, 2):
|
||||
continue
|
||||
# whisper.cpp counts every GPU/IGPU device, so never skip one here.
|
||||
backend_name = library.ggml_backend_dev_name(device) or b"unknown"
|
||||
description = library.ggml_backend_dev_description(device) or backend_name
|
||||
devices.append(
|
||||
{
|
||||
"index": len(devices),
|
||||
"name": description.decode("utf-8", errors="replace"),
|
||||
"backend_name": backend_name.decode("utf-8", errors="replace"),
|
||||
"device_type": "discrete" if device_type == 1 else "integrated",
|
||||
}
|
||||
)
|
||||
return devices
|
||||
except (ImportError, OSError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def _prefer_discrete_vulkan_device() -> Optional[int]:
|
||||
"""Return the index of the preferred hardware Vulkan GPU (discrete if available)."""
|
||||
devices = _hardware_vulkan_devices()
|
||||
|
||||
Reference in New Issue
Block a user