use candle_core::Device;
use super::candle_error;
use crate::config::Accelerator;
use crate::error::{OcrError, Result};
#[cfg(any(feature = "candle-metal", feature = "candle-cuda"))]
const DEVICE_ORDINAL: usize = 0;
pub(super) fn select_device(accelerator: Accelerator) -> Result<(Device, Accelerator)> {
match accelerator {
Accelerator::Cpu => Ok((Device::Cpu, Accelerator::Cpu)),
Accelerator::Auto => Ok(preferred_device()),
explicit => explicit_device(explicit),
}
}
fn preferred_device() -> (Device, Accelerator) {
for &candidate in preferred_accelerators() {
let Some(opened) = open(candidate) else {
tracing::debug!(
accelerator = candidate.as_str(),
feature = cargo_feature(candidate),
"skipping an accelerator this build was not compiled with"
);
continue;
};
match opened {
Ok(device) => {
tracing::info!(accelerator = candidate.as_str(), "opened the candle device");
return (device, candidate);
}
Err(error) => tracing::warn!(
accelerator = candidate.as_str(),
%error,
"the candle device could not be opened; trying the next candidate"
),
}
}
tracing::info!(
accelerator = "cpu",
"no accelerator available; running candle on the CPU"
);
(Device::Cpu, Accelerator::Cpu)
}
fn explicit_device(accelerator: Accelerator) -> Result<(Device, Accelerator)> {
let Some(opened) = open(accelerator) else {
return Err(OcrError::config(format!(
"accelerator `{}` is not compiled into this build of sceptre; rebuild with the `{}` cargo feature",
accelerator.as_str(),
cargo_feature(accelerator)
)));
};
let device = opened.map_err(|error| candle_error(&format!("open the `{}` device", accelerator.as_str()), error))?;
tracing::info!(accelerator = accelerator.as_str(), "opened the candle device");
Ok((device, accelerator))
}
fn open(accelerator: Accelerator) -> Option<candle_core::Result<Device>> {
match accelerator {
Accelerator::Metal => metal_device(),
Accelerator::Cuda => cuda_device(),
Accelerator::Cpu | Accelerator::Auto | Accelerator::CoreMl | Accelerator::DirectMl => None,
}
}
fn preferred_accelerators() -> &'static [Accelerator] {
#[cfg(any(target_os = "macos", target_os = "ios"))]
{
&[Accelerator::Metal]
}
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
{
&[Accelerator::Cuda]
}
}
fn cargo_feature(accelerator: Accelerator) -> &'static str {
match accelerator {
Accelerator::Metal => "candle-metal",
Accelerator::Cuda => "candle-cuda",
Accelerator::Cpu | Accelerator::Auto | Accelerator::CoreMl | Accelerator::DirectMl => "candle",
}
}
#[cfg(feature = "candle-metal")]
fn metal_device() -> Option<candle_core::Result<Device>> {
Some(Device::new_metal(DEVICE_ORDINAL))
}
#[cfg(not(feature = "candle-metal"))]
fn metal_device() -> Option<candle_core::Result<Device>> {
None
}
#[cfg(feature = "candle-cuda")]
fn cuda_device() -> Option<candle_core::Result<Device>> {
Some(Device::new_cuda(DEVICE_ORDINAL))
}
#[cfg(not(feature = "candle-cuda"))]
fn cuda_device() -> Option<candle_core::Result<Device>> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_open_the_cpu_without_touching_a_device() {
let (device, selected) = select_device(Accelerator::Cpu).expect("the cpu never fails");
assert!(device.is_cpu());
assert_eq!(selected, Accelerator::Cpu);
}
#[test]
fn should_resolve_auto_to_a_concrete_device() {
let (_device, selected) = select_device(Accelerator::Auto).expect("auto never fails");
assert_ne!(selected, Accelerator::Auto, "auto must resolve to a real device");
assert!(
Accelerator::Cpu == selected || crate::config::Backend::Candle.supports(selected),
"auto resolved to {selected:?}, which candle cannot run on"
);
}
#[test]
fn preferred_accelerators_never_list_cpu_or_auto() {
let preferred = preferred_accelerators();
assert!(!preferred.is_empty(), "every platform needs at least one candidate");
assert!(
preferred.iter().all(|candidate| crate::config::Backend::Candle
.hardware_accelerators()
.contains(candidate)),
"every candidate must be an accelerator candle claims to support: {preferred:?}"
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_platforms_prefer_metal() {
assert_eq!(preferred_accelerators(), &[Accelerator::Metal]);
}
#[test]
fn every_candle_device_names_the_cargo_feature_that_enables_it() {
assert_eq!(cargo_feature(Accelerator::Metal), "candle-metal");
assert_eq!(cargo_feature(Accelerator::Cuda), "candle-cuda");
}
#[test]
fn should_not_open_a_device_for_another_backends_accelerator() {
assert!(open(Accelerator::CoreMl).is_none());
assert!(open(Accelerator::DirectMl).is_none());
assert!(open(Accelerator::Cpu).is_none());
}
#[cfg(not(feature = "candle-cuda"))]
#[test]
fn should_reject_a_device_that_is_not_compiled_in() {
let Err(error) = select_device(Accelerator::Cuda) else {
panic!("cuda is not compiled in and must be rejected");
};
let message = error.to_string();
assert!(message.contains("cuda"), "message must name the accelerator: {message}");
assert!(
message.contains("candle-cuda"),
"message must name the feature: {message}"
);
}
#[cfg(feature = "candle-metal")]
#[test]
fn should_open_metal_when_compiled_in() {
let (device, selected) = select_device(Accelerator::Metal).expect("metal must open on this machine");
assert_eq!(selected, Accelerator::Metal);
assert!(device.is_metal());
}
}