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:
Jatin K Malik
2026-02-22 20:29:06 -08:00
committed by GitHub
parent 7656447995
commit 58b635b8b9
3 changed files with 254 additions and 21 deletions
+146 -8
View File
@@ -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.")
+56
View File
@@ -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()