#[cfg(feature = "cuda-provider")]
use ort::ep::CUDA;
#[cfg(feature = "directml-provider")]
use ort::ep::DirectML;
#[cfg(any(feature = "cuda-provider", feature = "directml-provider"))]
use ort::ep::ExecutionProvider;
use ort::ep::{CPU, ExecutionProviderDispatch};
use crate::{
config::ProviderPreference,
error::{RapidOcrError, Result},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResolvedExecutionProvider {
Cpu,
Cuda,
DirectMl,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProviderResolution {
pub requested: ProviderPreference,
pub selected_ep: ResolvedExecutionProvider,
pub fallback_used: bool,
}
#[derive(Debug)]
pub struct ProviderChain {
pub providers: Vec<ExecutionProviderDispatch>,
pub resolution: ProviderResolution,
}
pub fn ort_runtime_version() -> Option<String> {
unsafe {
let base = ort::sys::OrtGetApiBase();
if base.is_null() {
return None;
}
let raw = ((*base).GetVersionString)();
if raw.is_null() {
return None;
}
Some(std::ffi::CStr::from_ptr(raw).to_string_lossy().into_owned())
}
}
#[cfg_attr(
all(feature = "cuda-provider", feature = "directml-provider"),
allow(dead_code)
)]
fn feature_disabled(provider: &str, feature: &str) -> RapidOcrError {
RapidOcrError::UnsupportedProvider(format!(
"{provider} provider support is not compiled in; rebuild with `--features {feature}`"
))
}
#[cfg_attr(
not(any(feature = "cuda-provider", feature = "directml-provider")),
allow(dead_code)
)]
fn runtime_unavailable(provider: &str, detail: &str) -> String {
format!(
"{provider} is unavailable in the loaded ONNX Runtime ({detail}); install a \
{provider}-enabled ONNX Runtime runtime"
)
}
pub fn resolve_execution_providers(
preference: &ProviderPreference,
enable_cpu_mem_arena: bool,
fail_if_provider_unavailable: bool,
) -> Result<ProviderChain> {
let cpu_provider = || {
CPU::default()
.with_arena_allocator(enable_cpu_mem_arena)
.build()
};
match preference {
ProviderPreference::Cpu => Ok(ProviderChain {
providers: vec![cpu_provider()],
resolution: ProviderResolution {
requested: ProviderPreference::Cpu,
selected_ep: ResolvedExecutionProvider::Cpu,
fallback_used: false,
},
}),
ProviderPreference::Cuda { device_id } => {
#[cfg(feature = "cuda-provider")]
{
resolve_accelerator_execution_providers(
"CUDA",
"cuda-provider",
ProviderPreference::Cuda {
device_id: *device_id,
},
ResolvedExecutionProvider::Cuda,
*device_id,
cpu_provider(),
fail_if_provider_unavailable,
|id| {
let provider = CUDA::default().with_device_id(id);
let available = provider.is_available().map_err(|e| {
RapidOcrError::Config(runtime_unavailable("CUDA", &e.to_string()))
})?;
Ok((available, provider.build()))
},
)
}
#[cfg(not(feature = "cuda-provider"))]
{
let _ = (device_id, fail_if_provider_unavailable);
Err(feature_disabled("CUDA", "cuda-provider"))
}
}
ProviderPreference::DirectMl { device_id } => {
#[cfg(feature = "directml-provider")]
{
resolve_accelerator_execution_providers(
"DirectML",
"directml-provider",
ProviderPreference::DirectMl {
device_id: *device_id,
},
ResolvedExecutionProvider::DirectMl,
*device_id,
cpu_provider(),
fail_if_provider_unavailable,
|id| {
let provider = DirectML::default().with_device_id(id);
let available = match provider.is_available() {
Ok(available) => available,
Err(error) if fail_if_provider_unavailable => {
return Err(RapidOcrError::Config(runtime_unavailable(
"DirectML",
&error.to_string(),
)));
}
Err(_) => false,
};
Ok((available, provider.build()))
},
)
}
#[cfg(not(feature = "directml-provider"))]
{
if fail_if_provider_unavailable {
Err(feature_disabled("DirectML", "directml-provider"))
} else {
let _ = device_id;
Ok(ProviderChain {
providers: vec![cpu_provider()],
resolution: ProviderResolution {
requested: ProviderPreference::DirectMl {
device_id: *device_id,
},
selected_ep: ResolvedExecutionProvider::Cpu,
fallback_used: true,
},
})
}
}
}
}
}
#[cfg(any(feature = "cuda-provider", feature = "directml-provider"))]
fn device_id_to_i32(provider_name: &str, device_id: usize) -> Result<i32> {
i32::try_from(device_id).map_err(|_| {
RapidOcrError::Config(format!(
"invalid {provider_name} device_id {device_id}: value exceeds i32 range"
))
})
}
#[cfg(any(feature = "cuda-provider", feature = "directml-provider"))]
#[allow(clippy::too_many_arguments)]
fn resolve_accelerator_execution_providers<F>(
provider_name: &str,
_feature: &str,
requested: ProviderPreference,
preferred: ResolvedExecutionProvider,
device_id: usize,
cpu_provider: ExecutionProviderDispatch,
fail_if_provider_unavailable: bool,
prepare_provider: F,
) -> Result<ProviderChain>
where
F: FnOnce(i32) -> Result<(bool, ExecutionProviderDispatch)>,
{
let device_id_i32 = device_id_to_i32(provider_name, device_id)?;
let (is_available, provider_dispatch) = prepare_provider(device_id_i32)?;
let resolution = decide_provider_resolution(
requested,
preferred,
is_available,
fail_if_provider_unavailable,
)?;
if resolution.fallback_used {
Ok(ProviderChain {
providers: vec![cpu_provider],
resolution,
})
} else {
Ok(ProviderChain {
providers: vec![provider_dispatch, cpu_provider],
resolution,
})
}
}
#[cfg(any(feature = "cuda-provider", feature = "directml-provider"))]
fn decide_provider_resolution(
requested: ProviderPreference,
preferred: ResolvedExecutionProvider,
preferred_is_available: bool,
fail_if_provider_unavailable: bool,
) -> Result<ProviderResolution> {
if preferred_is_available {
return Ok(ProviderResolution {
requested,
selected_ep: preferred,
fallback_used: false,
});
}
if fail_if_provider_unavailable {
return Err(RapidOcrError::Config(strict_mode_message(requested)));
}
Ok(ProviderResolution {
requested,
selected_ep: ResolvedExecutionProvider::Cpu,
fallback_used: true,
})
}
#[cfg_attr(
not(any(feature = "cuda-provider", feature = "directml-provider")),
allow(dead_code)
)]
fn strict_mode_message(requested: ProviderPreference) -> String {
format!(
"requested execution provider {} is unavailable and fail_if_provider_unavailable=true",
format_provider_preference(requested)
)
}
#[cfg(test)]
fn require_requested_provider(resolution: ProviderResolution) -> Result<ProviderResolution> {
if resolution.fallback_used {
return Err(RapidOcrError::UnsupportedProvider(format!(
"requested execution provider {} is unavailable and this domain rejects silent CPU \
fallback; enable the provider feature, install its runtime, or request \
`ProviderPreference::Cpu` explicitly",
format_provider_preference(resolution.requested)
)));
}
Ok(resolution)
}
pub fn format_provider_preference(preference: ProviderPreference) -> String {
match preference {
ProviderPreference::Cpu => "cpu".to_string(),
ProviderPreference::Cuda { device_id } => format!("cuda(device_id={device_id})"),
ProviderPreference::DirectMl { device_id } => {
format!("directml(device_id={device_id})")
}
}
}
#[cfg(test)]
mod tests {
use super::{
ProviderResolution, ResolvedExecutionProvider, feature_disabled,
format_provider_preference, ort_runtime_version, require_requested_provider,
runtime_unavailable, strict_mode_message,
};
use crate::config::ProviderPreference;
use crate::error::RapidOcrError;
#[test]
fn every_remaining_provider_is_named_and_formatted() {
assert_eq!(format_provider_preference(ProviderPreference::Cpu), "cpu");
assert_eq!(
format_provider_preference(ProviderPreference::Cuda { device_id: 0 }),
"cuda(device_id=0)"
);
assert_eq!(
format_provider_preference(ProviderPreference::DirectMl { device_id: 1 }),
"directml(device_id=1)"
);
}
#[test]
fn feature_disabled_and_runtime_unavailable_are_distinguishable() {
let disabled = feature_disabled("CUDA", "cuda-provider").to_string();
assert!(disabled.contains("not compiled in"), "{disabled}");
assert!(disabled.contains("--features cuda-provider"), "{disabled}");
let unavailable = runtime_unavailable("DirectML", "provider library missing");
assert!(unavailable.contains("unavailable in the loaded ONNX Runtime"));
assert!(unavailable.contains("provider library missing"));
assert_ne!(disabled, unavailable);
}
#[test]
fn strict_mode_message_names_the_requested_provider() {
let message = strict_mode_message(ProviderPreference::Cuda { device_id: 3 });
assert!(message.contains("cuda(device_id=3)"), "{message}");
assert!(
message.contains("fail_if_provider_unavailable=true"),
"{message}"
);
}
#[test]
fn require_requested_provider_rejects_silent_cpu_fallback() {
let resolution = ProviderResolution {
requested: ProviderPreference::Cuda { device_id: 0 },
selected_ep: ResolvedExecutionProvider::Cpu,
fallback_used: true,
};
let error = require_requested_provider(resolution)
.expect_err("fallback must be rejected regardless of process-wide policy");
assert!(
matches!(error, RapidOcrError::UnsupportedProvider(_)),
"error: {error}"
);
assert!(
error.to_string().contains("cuda(device_id=0)"),
"error: {error}"
);
}
#[test]
fn require_requested_provider_accepts_selected_accelerator_and_cpu() {
let accelerated = ProviderResolution {
requested: ProviderPreference::DirectMl { device_id: 1 },
selected_ep: ResolvedExecutionProvider::DirectMl,
fallback_used: false,
};
assert_eq!(
require_requested_provider(accelerated)
.expect("a selected accelerator must pass")
.selected_ep,
ResolvedExecutionProvider::DirectMl
);
let cpu = ProviderResolution {
requested: ProviderPreference::Cpu,
selected_ep: ResolvedExecutionProvider::Cpu,
fallback_used: false,
};
assert!(
require_requested_provider(cpu).is_ok(),
"explicit CPU must always pass"
);
}
#[test]
fn ort_runtime_version_is_reported() {
let version = ort_runtime_version().expect("the linked ONNX Runtime must report a version");
assert!(
version.split('.').count() >= 2,
"version string should look like `1.17.0`, got {version:?}"
);
}
#[test]
#[cfg(not(feature = "directml-provider"))]
fn directml_without_feature_falls_back_or_reports_missing_feature_in_strict_mode() {
let error = super::resolve_execution_providers(
&ProviderPreference::DirectMl { device_id: 0 },
false,
false,
)
.expect("non-strict DirectML must fall back to CPU without the feature");
assert_eq!(error.resolution.selected_ep, ResolvedExecutionProvider::Cpu);
assert!(error.resolution.fallback_used);
let strict = super::resolve_execution_providers(
&ProviderPreference::DirectMl { device_id: 0 },
false,
true,
)
.expect_err("strict DirectML must report the missing feature");
let message = strict.to_string();
assert!(message.contains("not compiled in"), "{message}");
assert!(message.contains("directml-provider"), "{message}");
}
#[test]
#[cfg(not(feature = "cuda-provider"))]
fn cuda_without_feature_reports_the_missing_feature() {
let error = super::resolve_execution_providers(
&ProviderPreference::Cuda { device_id: 0 },
false,
false,
)
.expect_err("CUDA must not resolve without the feature");
let message = error.to_string();
assert!(message.contains("not compiled in"), "{message}");
assert!(message.contains("cuda-provider"), "{message}");
}
#[test]
#[cfg(feature = "directml-provider")]
fn directml_with_feature_resolves_or_reports_a_runtime_reason() {
match super::resolve_execution_providers(
&ProviderPreference::DirectMl { device_id: 0 },
false,
false,
) {
Ok(chain) => {
assert!(
!chain.providers.is_empty(),
"chain must contain CPU fallback"
);
}
Err(error) => {
let message = error.to_string();
assert!(
message.contains("unavailable in the loaded ONNX Runtime"),
"a runtime failure must say the runtime is unavailable: {message}"
);
}
}
}
#[test]
#[cfg(feature = "cuda-provider")]
fn cuda_with_feature_resolves_or_reports_a_runtime_reason() {
match super::resolve_execution_providers(
&ProviderPreference::Cuda { device_id: 0 },
false,
false,
) {
Ok(chain) => assert!(!chain.providers.is_empty()),
Err(error) => {
let message = error.to_string();
assert!(
message.contains("unavailable in the loaded ONNX Runtime"),
"a runtime failure must say the runtime is unavailable: {message}"
);
}
}
}
}