#[cfg(feature = "onnx")]
use ort::ep::ExecutionProvider as _;
#[derive(Debug, Clone)]
pub struct ProviderSelection {
provider: Provider,
is_requested: bool,
fallback_reason: Option<String>,
}
impl ProviderSelection {
pub fn name(&self) -> String {
self.provider.name()
}
pub fn fallback_name(&self) -> String {
self.provider.name()
}
pub fn reason(&self) -> String {
self.fallback_reason
.clone()
.unwrap_or_else(|| "no fallback".to_string())
}
pub fn is_requested_provider(&self) -> bool {
self.is_requested
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Provider {
Cpu,
Cuda,
Migraphx,
Rocm,
CoreMl,
}
impl Provider {
pub fn name(&self) -> String {
match self {
Provider::Cpu => "cpu".to_string(),
Provider::Cuda => "cuda".to_string(),
Provider::Migraphx => "migraphx".to_string(),
Provider::Rocm => "rocm".to_string(),
Provider::CoreMl => "coreml".to_string(),
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"cpu" => Some(Provider::Cpu),
"cuda" | "gpu" => Some(Provider::Cuda),
"migraphx" => Some(Provider::Migraphx),
"rocm" => Some(Provider::Rocm),
"coreml" => Some(Provider::CoreMl),
_ => None,
}
}
}
pub struct ExecutionProviderSelector;
impl ExecutionProviderSelector {
fn rocm_path_has_migraphx() -> bool {
std::env::var("ROCM_PATH")
.map(|p| {
let rocm = std::path::Path::new(&p);
rocm.join("lib/libmigraphx_c.so").exists()
|| rocm.join("bin/migraphx-driver").exists()
})
.unwrap_or(false)
}
pub fn select(requested: &str) -> Result<ProviderSelection, ProviderSelection> {
let requested = requested.trim().to_ascii_lowercase();
Self::select_normalized(&requested)
}
fn select_normalized(requested: &str) -> Result<ProviderSelection, ProviderSelection> {
if requested == "auto" {
let provider = select_auto_from_availability(
Self::is_coreml_available(),
Self::is_migraphx_available(),
Self::is_cuda_available(),
);
let reason = match provider {
Provider::CoreMl => "auto-detected CoreML (Apple GPU)".to_string(),
Provider::Migraphx => "auto-detected MIGraphX (AMD GPU)".to_string(),
Provider::Cuda => "auto-detected CUDA (NVIDIA GPU)".to_string(),
Provider::Cpu => "auto: no GPU execution provider available, using CPU".to_string(),
Provider::Rocm => "auto: using ROCm alias".to_string(),
};
return Ok(ProviderSelection {
provider,
is_requested: true,
fallback_reason: Some(reason),
});
}
match Provider::from_name(requested) {
Some(Provider::Cpu) => Ok(ProviderSelection {
provider: Provider::Cpu,
is_requested: true,
fallback_reason: None,
}),
Some(provider @ Provider::Cuda) => {
if Self::is_cuda_available() {
Ok(ProviderSelection {
provider,
is_requested: true,
fallback_reason: None,
})
} else {
Err(Self::cpu_fallback(
"CUDA runtime or driver not found on this system",
))
}
}
Some(Provider::Migraphx) => {
if Self::is_migraphx_available() {
Ok(ProviderSelection {
provider: Provider::Migraphx,
is_requested: true,
fallback_reason: None,
})
} else {
Err(Self::cpu_fallback(
"MIGraphX not found on this system (requires ROCm + MIGraphX)",
))
}
}
Some(Provider::Rocm) => {
if Self::is_migraphx_available() {
Ok(ProviderSelection {
provider: Provider::Migraphx,
is_requested: true,
fallback_reason: Some(
"ROCm EP is deprecated, using MIGraphX (the modern AMD GPU provider)"
.to_string(),
),
})
} else {
Err(Self::cpu_fallback(
"MIGraphX not found (rocm alias); ROCm EP is removed from ORT, \
install onnxruntime-migraphx or use CPU",
))
}
}
Some(provider @ Provider::CoreMl) => {
if Self::is_coreml_available() {
Ok(ProviderSelection {
provider,
is_requested: true,
fallback_reason: None,
})
} else {
Err(Self::cpu_fallback("CoreML is only available on macOS"))
}
}
None => Err(Self::cpu_fallback(&format!(
"unknown execution provider '{}', falling back to CPU",
requested
))),
}
}
fn cpu_fallback(reason: &str) -> ProviderSelection {
ProviderSelection {
provider: Provider::Cpu,
is_requested: false,
fallback_reason: Some(reason.to_string()),
}
}
fn is_cuda_available() -> bool {
#[cfg(feature = "onnx")]
{
ort::ep::CUDA::default().is_available().unwrap_or(false)
}
#[cfg(not(feature = "onnx"))]
{
std::env::var("CUDA_PATH").is_ok()
|| std::path::Path::new("/usr/bin/nvidia-smi").exists()
|| std::path::Path::new("/usr/local/cuda/bin/nvidia-smi").exists()
}
}
fn is_migraphx_available() -> bool {
#[cfg(feature = "onnx")]
{
if ort::ep::MIGraphX::default().is_available().unwrap_or(false) {
return true;
}
if std::path::Path::new("/opt/rocm/lib/libmigraphx_c.so").exists()
|| std::path::Path::new("/opt/rocm/bin/migraphx-driver").exists()
|| Self::rocm_path_has_migraphx()
{
tracing::debug!(
"MIGraphX not in GetAvailableProviders() but ROCm/MIGraphX \
libraries detected; will attempt registration"
);
return true;
}
false
}
#[cfg(not(feature = "onnx"))]
{
std::path::Path::new("/opt/rocm/bin/migraphx-driver").exists()
|| std::path::Path::new("/opt/rocm/lib/libmigraphx_c.so").exists()
|| Self::rocm_path_has_migraphx()
}
}
pub(crate) fn is_migraphx_compiled_in() -> bool {
#[cfg(feature = "onnx")]
{
ort::ep::MIGraphX::default().is_available().unwrap_or(false)
}
#[cfg(not(feature = "onnx"))]
{
false
}
}
pub(crate) fn is_cuda_compiled_in() -> bool {
#[cfg(feature = "onnx")]
{
ort::ep::CUDA::default().is_available().unwrap_or(false)
}
#[cfg(not(feature = "onnx"))]
{
false
}
}
fn is_coreml_available() -> bool {
cfg!(target_os = "macos")
}
}
pub fn is_migraphx_compiled_in() -> bool {
ExecutionProviderSelector::is_migraphx_compiled_in()
}
pub fn is_cuda_compiled_in() -> bool {
ExecutionProviderSelector::is_cuda_compiled_in()
}
pub fn select_auto_from_availability(coreml: bool, migraphx: bool, cuda: bool) -> Provider {
if coreml {
Provider::CoreMl
} else if migraphx {
Provider::Migraphx
} else if cuda {
Provider::Cuda
} else {
Provider::Cpu
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cpu_provider_always_available() {
let result = ExecutionProviderSelector::select("cpu");
assert!(result.is_ok());
let selection = result.unwrap();
assert_eq!(selection.name(), "cpu");
assert!(selection.is_requested_provider());
}
#[test]
fn test_cpu_provider_case_insensitive() {
let result = ExecutionProviderSelector::select("CPU");
assert!(result.is_ok());
assert_eq!(result.unwrap().name(), "cpu");
}
#[test]
fn test_unknown_provider_falls_back_to_cpu() {
let result = ExecutionProviderSelector::select("tpu");
assert!(result.is_err());
let fallback = result.unwrap_err();
assert_eq!(fallback.fallback_name(), "cpu");
assert!(!fallback.is_requested_provider());
assert!(fallback.reason().contains("unknown"));
}
#[test]
fn test_cuda_provider_selection() {
let result = ExecutionProviderSelector::select("cuda");
match result {
Ok(selection) => {
assert_eq!(selection.name(), "cuda");
assert!(selection.is_requested_provider());
}
Err(fallback) => {
assert_eq!(fallback.fallback_name(), "cpu");
assert!(!fallback.is_requested_provider());
assert!(fallback.reason().contains("CUDA"));
}
}
}
#[test]
fn test_gpu_alias_for_cuda() {
let result = ExecutionProviderSelector::select("gpu");
match result {
Ok(selection) => {
assert_eq!(selection.name(), "cuda");
}
Err(fallback) => {
assert_eq!(fallback.fallback_name(), "cpu");
}
}
}
#[test]
fn test_rocm_provider_selection() {
let result = ExecutionProviderSelector::select("rocm");
match result {
Ok(selection) => {
assert_eq!(selection.name(), "migraphx");
assert!(selection.reason().contains("MIGraphX"));
}
Err(fallback) => {
assert_eq!(fallback.fallback_name(), "cpu");
assert!(
fallback.reason().contains("ROCm") || fallback.reason().contains("MIGraphX")
);
}
}
}
#[test]
fn test_migraphx_provider_selection() {
let result = ExecutionProviderSelector::select("migraphx");
match result {
Ok(selection) => {
assert_eq!(selection.name(), "migraphx");
assert!(selection.is_requested_provider());
}
Err(fallback) => {
assert_eq!(fallback.fallback_name(), "cpu");
assert!(!fallback.is_requested_provider());
assert!(fallback.reason().contains("MIGraphX"));
}
}
}
#[test]
fn test_coreml_provider_selection() {
let result = ExecutionProviderSelector::select("coreml");
if cfg!(target_os = "macos") {
assert!(result.is_ok());
assert_eq!(result.unwrap().name(), "coreml");
} else {
assert!(result.is_err());
let fallback = result.unwrap_err();
assert_eq!(fallback.fallback_name(), "cpu");
assert!(fallback.reason().contains("macOS"));
}
}
#[test]
fn test_provider_from_name() {
assert_eq!(Provider::from_name("cpu"), Some(Provider::Cpu));
assert_eq!(Provider::from_name("CUDA"), Some(Provider::Cuda));
assert_eq!(Provider::from_name("migraphx"), Some(Provider::Migraphx));
assert_eq!(Provider::from_name("rocm"), Some(Provider::Rocm));
assert_eq!(Provider::from_name("CoreML"), Some(Provider::CoreMl));
assert_eq!(Provider::from_name("unknown"), None);
}
#[test]
fn test_provider_name() {
assert_eq!(Provider::Cpu.name(), "cpu");
assert_eq!(Provider::Cuda.name(), "cuda");
assert_eq!(Provider::Migraphx.name(), "migraphx");
assert_eq!(Provider::Rocm.name(), "rocm");
assert_eq!(Provider::CoreMl.name(), "coreml");
}
#[test]
fn test_provider_selection_fields() {
let sel = ProviderSelection {
provider: Provider::Cpu,
is_requested: true,
fallback_reason: None,
};
assert_eq!(sel.name(), "cpu");
assert!(sel.is_requested_provider());
assert_eq!(sel.reason(), "no fallback");
}
#[test]
fn test_rocm_path_migraphx_detection_checks_lib_and_bin() {
let temp = tempfile::tempdir().unwrap();
let rocm_lib = temp.path().join("lib");
std::fs::create_dir_all(&rocm_lib).unwrap();
std::fs::write(rocm_lib.join("libmigraphx_c.so"), b"fake").unwrap();
let old_rocm = std::env::var("ROCM_PATH").ok();
unsafe { std::env::set_var("ROCM_PATH", temp.path()) };
assert!(ExecutionProviderSelector::rocm_path_has_migraphx());
if let Some(value) = old_rocm {
unsafe { std::env::set_var("ROCM_PATH", value) };
} else {
unsafe { std::env::remove_var("ROCM_PATH") };
}
}
#[test]
fn test_is_migraphx_compiled_in_does_not_panic() {
let _ = ExecutionProviderSelector::is_migraphx_compiled_in();
}
#[test]
fn test_is_cuda_compiled_in_does_not_panic() {
let _ = ExecutionProviderSelector::is_cuda_compiled_in();
}
#[test]
fn test_compiled_in_probes_cannot_simultaneously_be_true() {
let migraphx = ExecutionProviderSelector::is_migraphx_compiled_in();
let cuda = ExecutionProviderSelector::is_cuda_compiled_in();
assert!(
!(migraphx && cuda),
"MIGraphX={} and CUDA={} both reported as compiled in; \
a single ORT binary cannot contain both providers.",
migraphx,
cuda
);
}
#[test]
fn test_compiled_in_subset_of_heuristic_available() {
let migraphx_compiled = ExecutionProviderSelector::is_migraphx_compiled_in();
let migraphx_available = ExecutionProviderSelector::is_migraphx_available();
assert!(
!migraphx_compiled || migraphx_available,
"is_migraphx_compiled_in=true is more restrictive than \
is_migraphx_available; the pure probe must not say true when \
the heuristic path says false"
);
}
#[test]
fn test_free_function_helpers_match_method_form() {
assert_eq!(
crate::embed::provider::is_migraphx_compiled_in(),
ExecutionProviderSelector::is_migraphx_compiled_in()
);
assert_eq!(
crate::embed::provider::is_cuda_compiled_in(),
ExecutionProviderSelector::is_cuda_compiled_in()
);
}
#[test]
fn auto_order() {
assert_eq!(
select_auto_from_availability(true, true, true),
Provider::CoreMl
);
assert_eq!(
select_auto_from_availability(false, true, true),
Provider::Migraphx
);
assert_eq!(
select_auto_from_availability(false, false, true),
Provider::Cuda
);
assert_eq!(
select_auto_from_availability(false, false, false),
Provider::Cpu
);
}
#[test]
fn auto_returns_ok_not_fallback_on_cpu_only() {
let result = ExecutionProviderSelector::select("auto");
match result {
Ok(selection) => {
let name = selection.name();
assert!(
name == "cpu" || name == "cuda" || name == "migraphx" || name == "coreml",
"auto resolved to {name}, expected a concrete provider"
);
assert!(
selection.is_requested_provider(),
"auto resolution is always 'requested'"
);
if name == "cpu" {
assert!(
selection.reason().contains("no GPU"),
"auto→cpu reason should explain no GPU was found, got: {}",
selection.reason()
);
}
}
Err(fallback) => {
panic!(
"auto must never return Err (got CPU fallback: {})",
fallback.reason()
);
}
}
}
#[test]
fn auto_never_resolves_to_unresolved_name() {
let selection = ExecutionProviderSelector::select("auto").unwrap();
assert_ne!(selection.name(), "auto");
assert_ne!(selection.name(), "rocm");
}
#[test]
fn select_normalizes_whitespace_and_case() {
for input in [" CUDA ", "Cuda", "GPU", " MiGrApHx ", "\tcoreml\n"] {
let _ = ExecutionProviderSelector::select(input);
let result = ExecutionProviderSelector::select(input);
let name = match &result {
Ok(s) => s.name(),
Err(s) => s.fallback_name(),
};
assert!(
!name.contains("unknown"),
"normalized '{input}' should not hit the unknown-fallback"
);
}
}
#[test]
fn rocm_alias_never_returns_rocm_provider() {
for input in ["rocm", " ROCM ", "Rocm"] {
match ExecutionProviderSelector::select(input) {
Ok(s) => assert_eq!(s.name(), "migraphx", "rocm alias resolved to {}", s.name()),
Err(f) => assert_eq!(f.fallback_name(), "cpu"),
}
}
}
}