fix(rb-07): runtime capabilities derive accelerators honestly

get_runtime_capabilities was returning `accelerators = ["cpu",
"vulkan"]` and `whisper.supports_gpu = true` regardless of build
config or runtime state. On a macOS build it falsely advertised
"vulkan" (the backend actually resolves via MoltenVK as Metal); on a
whisper-disabled build it claimed GPU support for an engine that
hadn't been linked.

Added `compose_accelerators(whisper_enabled, loader_available, target)`
— a pure helper that always emits "cpu" first and appends the
platform-appropriate GPU name only when whisper is compiled in AND the
Vulkan loader resolves. `supported_accelerators()` wraps it with the
live `cfg!(feature = "whisper")`, loader probe, and target OS.

`get_runtime_capabilities` now calls `supported_accelerators()` and
sets `whisper.supports_gpu = cfg!(feature = "whisper")`. Parakeet
stays CPU-only.

Five tests in `commands::models::tests` cover the permutation matrix:
whisper on/off, loader present/missing, macOS vs other. Both feature
configurations (`--features whisper` and `--no-default-features`)
build and pass tests.

Macos Metal-loader resolution on real hardware stays on the
ship-gate checklist — the detection logic is verifiable from Linux
but runtime behaviour is not.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 10:32:04 +01:00
parent 05d823bf05
commit 54d9adf1f0
3 changed files with 151 additions and 8 deletions

View File

@@ -341,6 +341,51 @@ fn vulkan_loader_available() -> bool {
false
}
/// Compile-time target signalling used by `compose_accelerators`.
/// Split out so the pure-function behaviour is testable without `cfg!`
/// appearing in the test matrix.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AcceleratorTarget {
Macos,
Other,
}
/// Pure helper: produce the `accelerators` list from the
/// whisper-compiled-in flag, runtime loader availability, and target
/// family. Always starts with `cpu`; appends the platform-appropriate
/// GPU name only when whisper is compiled in AND the Vulkan loader
/// resolves. RB-07 replaced a hard-coded `["cpu", "vulkan"]`.
fn compose_accelerators(
whisper_enabled: bool,
loader_available: bool,
target: AcceleratorTarget,
) -> Vec<String> {
let mut accelerators = vec!["cpu".into()];
if whisper_enabled && loader_available {
let gpu = match target {
AcceleratorTarget::Macos => "metal",
AcceleratorTarget::Other => "vulkan",
};
accelerators.push(gpu.into());
}
accelerators
}
/// Public wrapper around `compose_accelerators` that reads the real
/// `cfg(feature = "whisper")`, runtime loader probe, and target OS.
fn supported_accelerators() -> Vec<String> {
let target = if cfg!(target_os = "macos") {
AcceleratorTarget::Macos
} else {
AcceleratorTarget::Other
};
compose_accelerators(
cfg!(feature = "whisper"),
vulkan_loader_available(),
target,
)
}
/// Report which backend whisper.cpp was actually able to initialise
/// against. The whisper-rs build here is compiled with the `vulkan`
/// feature unconditionally; on macOS that's still Metal via MoltenVK,
@@ -453,12 +498,14 @@ pub fn get_runtime_capabilities(
let active_compute_device = detect_active_compute_device();
let cpu_features: CpuFeaturesInfo = hardware::probe_cpu_features().into();
// Kon's desktop build links transcribe-rs with the `whisper-vulkan`
// feature unconditionally (see crates/transcription/Cargo.toml), so
// whisper.cpp boots with Vulkan backend on any machine with a Vulkan
// loader + ICD. Parakeet (ONNX) still runs on CPU. Reflect both.
// Accelerator list is now derived from the live build configuration
// and runtime loader probe — see `compose_accelerators`. Parakeet
// (ONNX) stays CPU-only; Whisper advertises `supports_gpu` only
// when it was actually compiled in for this binary.
let whisper_supports_gpu = cfg!(feature = "whisper");
Ok(RuntimeCapabilities {
accelerators: vec!["cpu".into(), "vulkan".into()],
accelerators: supported_accelerators(),
engines: vec![
EngineRuntimeCapabilities {
id: "whisper".into(),
@@ -466,7 +513,7 @@ pub fn get_runtime_capabilities(
loaded_model_id: whisper
.loaded_model_id()
.map(|model_id| model_id.to_string()),
supports_gpu: true,
supports_gpu: whisper_supports_gpu,
models: whisper_models,
},
EngineRuntimeCapabilities {
@@ -594,3 +641,66 @@ pub async fn load_parakeet_model(
pub fn check_parakeet_engine(state: tauri::State<'_, AppState>) -> Result<bool, String> {
Ok(state.parakeet_engine.is_loaded())
}
#[cfg(test)]
mod tests {
use super::*;
// RB-07: the accelerator list is now derived from compile flags +
// runtime loader probe + target. These cover the permutations.
//
// Pre-fix behaviour was `vec!["cpu".into(), "vulkan".into()]`
// regardless of whisper feature, loader availability, or platform —
// so a macOS build falsely advertised "vulkan", and a no-whisper
// build falsely advertised a GPU path with no engine to use it.
#[test]
fn cpu_only_when_whisper_disabled() {
assert_eq!(
compose_accelerators(false, true, AcceleratorTarget::Macos),
vec!["cpu".to_string()]
);
assert_eq!(
compose_accelerators(false, true, AcceleratorTarget::Other),
vec!["cpu".to_string()]
);
}
#[test]
fn cpu_only_when_loader_missing() {
assert_eq!(
compose_accelerators(true, false, AcceleratorTarget::Macos),
vec!["cpu".to_string()]
);
assert_eq!(
compose_accelerators(true, false, AcceleratorTarget::Other),
vec!["cpu".to_string()]
);
}
#[test]
fn macos_with_loader_advertises_metal() {
assert_eq!(
compose_accelerators(true, true, AcceleratorTarget::Macos),
vec!["cpu".to_string(), "metal".to_string()]
);
}
#[test]
fn non_macos_with_loader_advertises_vulkan() {
assert_eq!(
compose_accelerators(true, true, AcceleratorTarget::Other),
vec!["cpu".to_string(), "vulkan".to_string()]
);
}
#[test]
fn cpu_is_always_first_entry() {
// Frontend relies on index-0 being the fallback; preserve that
// contract regardless of which GPU extras are added.
for target in [AcceleratorTarget::Macos, AcceleratorTarget::Other] {
let full = compose_accelerators(true, true, target);
assert_eq!(full.first(), Some(&"cpu".to_string()));
}
}
}