feat: detect incompatible Intel GPUs and fallback to CPU (#239)
* feat: detect incompatible Intel GPUs and fallback to CPU Implements two-layer protection for users with older Intel GPUs that lack VK_KHR_16bit_storage support required by whisper.cpp Vulkan backend. Affected GPUs: Intel Gen7 and older (Ivy Bridge, Haswell, Sandy Bridge) - Intel HD Graphics 2500/4000 (Ivy Bridge) - Intel HD Graphics 4400/4600 (Haswell) Layer 1: Install-Time Detection (Prevention) - New check_vulkan_gpu_compatibility() function in install.sh - Parses GPU name from vulkaninfo output - Checks against known incompatible patterns - Shows clear warning and recommends CPU backend - Updates detect_whispercpp_backends() to skip Vulkan for incompatible GPUs Layer 2: Runtime Fallback (Safety Net) - Added error handling in recognition_manager.py _init_whispercpp() - Catches RuntimeError with "16-bit storage" or "Unsupported device" - Automatically disables Vulkan/CUDA and retries with CPU - Shows desktop notification explaining the fallback Fixes #238 * fix(gpu): improve compatibility detection and add tests
This commit is contained in:
+146
-8
@@ -52,11 +52,11 @@ get_vocalinux_pids() {
|
||||
check_running_processes() {
|
||||
local PIDS
|
||||
PIDS=$(get_vocalinux_pids || true)
|
||||
|
||||
|
||||
if [ -n "$PIDS" ]; then
|
||||
print_warning "Found running Vocalinux process(es): $PIDS"
|
||||
echo ""
|
||||
|
||||
|
||||
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||||
print_info "Non-interactive mode: stopping Vocalinux automatically..."
|
||||
else
|
||||
@@ -68,20 +68,20 @@ check_running_processes() {
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
print_info "Stopping Vocalinux..."
|
||||
echo "$PIDS" | xargs -r kill -TERM 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
|
||||
local REMAINING_PIDS
|
||||
REMAINING_PIDS=$(get_vocalinux_pids || true)
|
||||
|
||||
|
||||
if [ -n "$REMAINING_PIDS" ]; then
|
||||
print_warning "Some processes still running, forcing termination..."
|
||||
echo "$REMAINING_PIDS" | xargs -r kill -KILL 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
|
||||
local FINAL_PIDS
|
||||
FINAL_PIDS=$(get_vocalinux_pids || true)
|
||||
if [ -n "$FINAL_PIDS" ]; then
|
||||
@@ -479,6 +479,109 @@ detect_vulkan() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check for incompatible Intel GPUs that don't support VK_KHR_16bit_storage
|
||||
# These GPUs will fail with "device does not support 16-bit storage" error
|
||||
# Affected: Intel Gen7 and older (Ivy Bridge, Haswell, Sandy Bridge)
|
||||
# See: https://github.com/jatinkrmalik/vocalinux/issues/238
|
||||
check_vulkan_gpu_compatibility() {
|
||||
# List of known incompatible GPU patterns
|
||||
local INCOMPATIBLE_PATTERNS=(
|
||||
"Ivy Bridge"
|
||||
"Haswell"
|
||||
"Sandy Bridge"
|
||||
"HD Graphics 2500"
|
||||
"HD Graphics 4000"
|
||||
"HD Graphics 4400"
|
||||
"HD Graphics 4600"
|
||||
"HD Graphics P4600"
|
||||
"HD Graphics P4700"
|
||||
"IVB"
|
||||
"HSW"
|
||||
"SNB"
|
||||
)
|
||||
|
||||
# Check if vulkaninfo is available
|
||||
if ! command -v vulkaninfo >/dev/null 2>&1; then
|
||||
echo "unknown:vulkaninfo not available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local DEVICE_NAMES_RAW
|
||||
DEVICE_NAMES_RAW=$(vulkaninfo --summary 2>/dev/null | awk -F'=' '/deviceName/ {gsub(/^[ \t]+|[ \t]+$/, "", $2); if ($2 != "") print $2}')
|
||||
|
||||
local DEVICE_NAMES_LABEL=""
|
||||
while IFS= read -r device_name; do
|
||||
[ -z "$device_name" ] && continue
|
||||
if [ -n "$DEVICE_NAMES_LABEL" ]; then
|
||||
DEVICE_NAMES_LABEL="${DEVICE_NAMES_LABEL}, ${device_name}"
|
||||
else
|
||||
DEVICE_NAMES_LABEL="$device_name"
|
||||
fi
|
||||
done <<< "$DEVICE_NAMES_RAW"
|
||||
|
||||
local FEATURES_OUTPUT
|
||||
FEATURES_OUTPUT=$(vulkaninfo --features 2>/dev/null)
|
||||
|
||||
if [ -n "$FEATURES_OUTPUT" ]; then
|
||||
if echo "$FEATURES_OUTPUT" | grep -Eq "VK_KHR_16bit_storage|storageBuffer16BitAccess[[:space:]]*=[[:space:]]*true|uniformAndStorageBuffer16BitAccess[[:space:]]*=[[:space:]]*true|storagePushConstant16[[:space:]]*=[[:space:]]*true"; then
|
||||
if [ -z "$DEVICE_NAMES_LABEL" ]; then
|
||||
DEVICE_NAMES_LABEL="Vulkan GPU (VK_KHR_16bit_storage)"
|
||||
fi
|
||||
echo "compatible:${DEVICE_NAMES_LABEL}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "$DEVICE_NAMES_LABEL" ]; then
|
||||
DEVICE_NAMES_LABEL="Vulkan GPU"
|
||||
fi
|
||||
echo "incompatible:${DEVICE_NAMES_LABEL} (missing VK_KHR_16bit_storage)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$DEVICE_NAMES_RAW" ]; then
|
||||
echo "unknown:Could not detect GPU name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local HAS_COMPATIBLE_GPU=false
|
||||
local INCOMPATIBLE_LABEL=""
|
||||
|
||||
while IFS= read -r device_name; do
|
||||
[ -z "$device_name" ] && continue
|
||||
|
||||
local is_incompatible=false
|
||||
for pattern in "${INCOMPATIBLE_PATTERNS[@]}"; do
|
||||
if echo "$device_name" | grep -iq "$pattern"; then
|
||||
is_incompatible=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$is_incompatible" = true ]; then
|
||||
if [ -n "$INCOMPATIBLE_LABEL" ]; then
|
||||
INCOMPATIBLE_LABEL="${INCOMPATIBLE_LABEL}, ${device_name}"
|
||||
else
|
||||
INCOMPATIBLE_LABEL="$device_name"
|
||||
fi
|
||||
else
|
||||
HAS_COMPATIBLE_GPU=true
|
||||
fi
|
||||
done <<< "$DEVICE_NAMES_RAW"
|
||||
|
||||
if [ "$HAS_COMPATIBLE_GPU" = true ]; then
|
||||
echo "compatible:${DEVICE_NAMES_LABEL}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "$INCOMPATIBLE_LABEL" ]; then
|
||||
echo "incompatible:${INCOMPATIBLE_LABEL}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "unknown:Could not classify Vulkan GPU compatibility"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Detect available GPU backends for whisper.cpp and recommend the best option
|
||||
detect_whispercpp_backends() {
|
||||
detect_nvidia_gpu || true
|
||||
@@ -496,12 +599,23 @@ detect_whispercpp_backends() {
|
||||
HAS_CUDA_DEV=true
|
||||
fi
|
||||
|
||||
# Check Vulkan GPU compatibility (Gen7 and older Intel GPUs lack 16-bit storage support)
|
||||
local VULKAN_COMPATIBLE="unknown"
|
||||
local VULKAN_COMPAT_REASON=""
|
||||
if [[ "$HAS_VULKAN" == "yes" ]]; then
|
||||
local COMPAT_RESULT
|
||||
COMPAT_RESULT=$(check_vulkan_gpu_compatibility)
|
||||
VULKAN_COMPATIBLE=$(echo "$COMPAT_RESULT" | cut -d':' -f1)
|
||||
VULKAN_COMPAT_REASON=$(echo "$COMPAT_RESULT" | cut -d':' -f2-)
|
||||
fi
|
||||
|
||||
# Determine recommendation
|
||||
local RECOMMENDED_BACKEND="cpu"
|
||||
local RECOMMENDED_REASON=""
|
||||
local CAN_BUILD_GPU=false
|
||||
|
||||
if [[ "$HAS_VULKAN" == "yes" && "$HAS_VULKAN_DEV" == "true" ]]; then
|
||||
# Only recommend Vulkan if GPU is compatible
|
||||
if [[ "$HAS_VULKAN" == "yes" && "$HAS_VULKAN_DEV" == "true" && "$VULKAN_COMPATIBLE" == "compatible" ]]; then
|
||||
RECOMMENDED_BACKEND="vulkan"
|
||||
RECOMMENDED_REASON="Vulkan GPU detected with dev libraries"
|
||||
CAN_BUILD_GPU=true
|
||||
@@ -509,6 +623,10 @@ detect_whispercpp_backends() {
|
||||
RECOMMENDED_BACKEND="cuda"
|
||||
RECOMMENDED_REASON="NVIDIA GPU with CUDA toolkit"
|
||||
CAN_BUILD_GPU=true
|
||||
elif [[ "$VULKAN_COMPATIBLE" == "incompatible" ]]; then
|
||||
RECOMMENDED_BACKEND="cpu"
|
||||
RECOMMENDED_REASON="Incompatible GPU ($VULKAN_COMPAT_REASON) - CPU mode recommended"
|
||||
CAN_BUILD_GPU=false
|
||||
elif [[ "$HAS_VULKAN" == "yes" || "$HAS_NVIDIA_GPU" == "yes" ]]; then
|
||||
RECOMMENDED_BACKEND="cpu"
|
||||
if [[ "$HAS_VULKAN" == "yes" ]]; then
|
||||
@@ -523,7 +641,7 @@ detect_whispercpp_backends() {
|
||||
CAN_BUILD_GPU=false
|
||||
fi
|
||||
|
||||
echo "${RECOMMENDED_BACKEND}:${RECOMMENDED_REASON}:${CAN_BUILD_GPU}:${HAS_VULKAN}:${HAS_NVIDIA_GPU}:${HAS_VULKAN_DEV}:${HAS_CUDA_DEV}"
|
||||
echo "${RECOMMENDED_BACKEND}:${RECOMMENDED_REASON}:${CAN_BUILD_GPU}:${HAS_VULKAN}:${HAS_NVIDIA_GPU}:${HAS_VULKAN_DEV}:${HAS_CUDA_DEV}:${VULKAN_COMPATIBLE}:${VULKAN_COMPAT_REASON}"
|
||||
}
|
||||
|
||||
# Detect hardware and recommend best engine
|
||||
@@ -711,6 +829,26 @@ EOF
|
||||
local HAS_NVIDIA=$(echo "$BACKEND_INFO" | cut -d':' -f5)
|
||||
local HAS_VULKAN_DEV=$(echo "$BACKEND_INFO" | cut -d':' -f6)
|
||||
local HAS_CUDA_DEV=$(echo "$BACKEND_INFO" | cut -d':' -f7)
|
||||
local VULKAN_COMPAT=$(echo "$BACKEND_INFO" | cut -d':' -f8)
|
||||
local VULKAN_COMPAT_REASON=$(echo "$BACKEND_INFO" | cut -d':' -f9)
|
||||
|
||||
# Show warning for incompatible GPUs
|
||||
if [[ "$VULKAN_COMPAT" == "incompatible" ]]; then
|
||||
echo ""
|
||||
print_warning "═══════════════════════════════════════════════════════════════"
|
||||
print_warning " ⚠️ INCOMPATIBLE GPU DETECTED"
|
||||
print_warning "═══════════════════════════════════════════════════════════════"
|
||||
print_warning ""
|
||||
print_warning " Your GPU: $VULKAN_COMPAT_REASON"
|
||||
print_warning ""
|
||||
print_warning " This Intel GPU lacks VK_KHR_16bit_storage support, which is"
|
||||
print_warning " required for whisper.cpp Vulkan acceleration."
|
||||
print_warning ""
|
||||
print_warning " The CPU backend will be used instead, which is still fast!"
|
||||
print_warning ""
|
||||
print_warning "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Whisper.cpp can use different backends for speech recognition:"
|
||||
echo ""
|
||||
|
||||
@@ -17,11 +17,7 @@ from typing import Callable, List, Optional
|
||||
from ..common_types import RecognitionState
|
||||
from ..ui.audio_feedback import play_error_sound, play_start_sound, play_stop_sound
|
||||
from ..utils.vosk_model_info import VOSK_MODEL_INFO
|
||||
from ..utils.whispercpp_model_info import (
|
||||
WHISPERCPP_MODEL_INFO,
|
||||
get_model_path,
|
||||
is_model_downloaded,
|
||||
)
|
||||
from ..utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO, get_model_path, is_model_downloaded
|
||||
from .command_processor import CommandProcessor
|
||||
|
||||
|
||||
@@ -692,19 +688,62 @@ class SpeechRecognitionManager:
|
||||
cpu_count = multiprocessing.cpu_count()
|
||||
|
||||
load_start_time = time.time()
|
||||
self.model = Model(
|
||||
model_path,
|
||||
n_threads=n_threads,
|
||||
suppress_blank=True,
|
||||
no_speech_thold=0.6,
|
||||
entropy_thold=2.4,
|
||||
)
|
||||
|
||||
loaded_backend = backend
|
||||
|
||||
# Attempt to load model with automatic backend selection
|
||||
# If Vulkan GPU fails (e.g., incompatible Intel GPU), fallback to CPU
|
||||
try:
|
||||
self.model = Model(
|
||||
model_path,
|
||||
n_threads=n_threads,
|
||||
suppress_blank=True,
|
||||
no_speech_thold=0.6,
|
||||
entropy_thold=2.4,
|
||||
)
|
||||
except RuntimeError as model_error:
|
||||
error_str = str(model_error).lower()
|
||||
# Check for Vulkan 16-bit storage incompatibility
|
||||
if (
|
||||
"16-bit storage" in error_str
|
||||
or "unsupported device" in error_str
|
||||
or "incompatible driver" in error_str
|
||||
):
|
||||
logger.warning(
|
||||
f"Vulkan GPU initialization failed: {model_error}. "
|
||||
"Falling back to CPU backend."
|
||||
)
|
||||
_show_notification(
|
||||
"Vocalinux: GPU Fallback",
|
||||
"Your GPU doesn't support whisper.cpp Vulkan.\n"
|
||||
"Switched to CPU mode - still fast!",
|
||||
"dialog-information",
|
||||
)
|
||||
# Force CPU backend by disabling GPU backends
|
||||
os.environ["GGML_VULKAN"] = "0"
|
||||
os.environ["GGML_CUDA"] = "0"
|
||||
# Retry with CPU-only backend
|
||||
self.model = Model(
|
||||
model_path,
|
||||
n_threads=n_threads,
|
||||
suppress_blank=True,
|
||||
no_speech_thold=0.6,
|
||||
entropy_thold=2.4,
|
||||
)
|
||||
loaded_backend = ComputeBackend.CPU
|
||||
backend_info = "CPU (fallback from incompatible Vulkan GPU)"
|
||||
logger.info("Successfully loaded model with CPU backend")
|
||||
else:
|
||||
raise
|
||||
|
||||
load_duration = time.time() - load_start_time
|
||||
|
||||
logger.info(
|
||||
f"whisper.cpp configured with n_threads={n_threads} (detected {cpu_count} CPUs)"
|
||||
)
|
||||
logger.info(f"whisper.cpp model loaded in {load_duration:.2f}s ({backend} backend)")
|
||||
logger.info(
|
||||
f"whisper.cpp model loaded in {load_duration:.2f}s ({loaded_backend} backend)"
|
||||
)
|
||||
|
||||
self._model_initialized = True
|
||||
logger.info("whisper.cpp engine initialized successfully.")
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
Tests for Whisper speech recognition support.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from unittest.mock import MagicMock, patch # noqa: F401
|
||||
|
||||
# Mock GTK and other dependencies before importing vocalinux
|
||||
@@ -103,3 +105,57 @@ class TestWhisperSupport:
|
||||
assert manager.engine == "whisper"
|
||||
assert manager.model_size == "base"
|
||||
whisper_mock.load_model.assert_called_once()
|
||||
|
||||
def test_whispercpp_falls_back_to_cpu_on_16bit_storage_error(self):
|
||||
model_ctor = MagicMock(
|
||||
side_effect=[
|
||||
RuntimeError("device does not support 16-bit storage"),
|
||||
MagicMock(),
|
||||
]
|
||||
)
|
||||
|
||||
pywhispercpp_pkg = types.ModuleType("pywhispercpp")
|
||||
pywhispercpp_model = types.ModuleType("pywhispercpp.model")
|
||||
setattr(pywhispercpp_model, "Model", model_ctor)
|
||||
setattr(pywhispercpp_pkg, "model", pywhispercpp_model)
|
||||
|
||||
psutil_mock = MagicMock()
|
||||
psutil_mock.virtual_memory.return_value = MagicMock(total=8 * (1024**3))
|
||||
|
||||
with patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"pywhispercpp": pywhispercpp_pkg,
|
||||
"pywhispercpp.model": pywhispercpp_model,
|
||||
"psutil": psutil_mock,
|
||||
},
|
||||
):
|
||||
from vocalinux.speech_recognition import recognition_manager as rm
|
||||
|
||||
with patch("os.makedirs"), patch("os.path.exists", return_value=True), patch(
|
||||
"os.path.getsize", return_value=40 * 1024 * 1024
|
||||
), patch("multiprocessing.cpu_count", return_value=4), patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend",
|
||||
return_value=("vulkan", "Intel GPU"),
|
||||
), patch(
|
||||
"vocalinux.utils.whispercpp_model_info.get_backend_display_name",
|
||||
return_value="Vulkan",
|
||||
), patch(
|
||||
"vocalinux.speech_recognition.recognition_manager.get_model_path",
|
||||
return_value="/tmp/mock-ggml-tiny.bin",
|
||||
), patch(
|
||||
"vocalinux.speech_recognition.recognition_manager._show_notification"
|
||||
) as notify_mock, patch.dict(
|
||||
os.environ,
|
||||
{"GGML_VULKAN": "1", "GGML_CUDA": "1"},
|
||||
clear=False,
|
||||
):
|
||||
manager = rm.SpeechRecognitionManager(
|
||||
engine="whisper_cpp", model_size="tiny", defer_download=False
|
||||
)
|
||||
|
||||
assert manager.model is not None
|
||||
assert model_ctor.call_count == 2
|
||||
assert os.environ["GGML_VULKAN"] == "0"
|
||||
assert os.environ["GGML_CUDA"] == "0"
|
||||
notify_mock.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user