use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub enum OrtGraphOptimizationLevel {
DisableAll,
#[default]
Level1,
Level2,
Level3,
All,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum OrtCoreMLComputeUnits {
#[default]
All,
CPUAndGPU,
CPUAndNeuralEngine,
CPUOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum OrtCoreMLModelFormat {
#[default]
MLProgram,
NeuralNetwork,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum OrtCoreMLSpecializationStrategy {
#[default]
Default,
FastPrediction,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct OrtCoreMLConfig {
pub compute_units: Option<OrtCoreMLComputeUnits>,
pub model_format: Option<OrtCoreMLModelFormat>,
pub static_input_shapes: Option<bool>,
pub specialization_strategy: Option<OrtCoreMLSpecializationStrategy>,
pub allow_low_precision_accumulation_on_gpu: Option<bool>,
pub profile_compute_plan: Option<bool>,
pub model_cache_dir: Option<String>,
}
pub(crate) const COREML_CONFIG_ENTRY: &str = "oar.internal.coreml_config";
pub(crate) const AUTO_DEVICE_CONFIG_ENTRY: &str = "oar.internal.auto_device";
fn auto_signature(providers: &[OrtExecutionProvider]) -> String {
let device = |id: &Option<i32>| id.map_or_else(String::new, |id| format!(":{id}"));
providers
.iter()
.map(|provider| match provider {
OrtExecutionProvider::CPU => "cpu".to_string(),
OrtExecutionProvider::CUDA { device_id, .. } => format!("cuda{}", device(device_id)),
OrtExecutionProvider::DirectML { device_id } => {
format!("directml{}", device(device_id))
}
OrtExecutionProvider::OpenVINO { .. } => "openvino".to_string(),
OrtExecutionProvider::TensorRT { device_id, .. } => {
format!("tensorrt{}", device(device_id))
}
OrtExecutionProvider::CoreML { .. } => "coreml".to_string(),
OrtExecutionProvider::WebGPU => "webgpu".to_string(),
})
.collect::<Vec<_>>()
.join(",")
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub enum OrtExecutionProvider {
#[default]
CPU,
CUDA {
device_id: Option<i32>,
gpu_mem_limit: Option<usize>,
arena_extend_strategy: Option<String>,
cudnn_conv_algo_search: Option<String>,
cudnn_conv_use_max_workspace: Option<bool>,
},
DirectML {
device_id: Option<i32>,
},
OpenVINO {
device_type: Option<String>,
num_threads: Option<usize>,
},
TensorRT {
device_id: Option<i32>,
max_workspace_size: Option<usize>,
min_subgraph_size: Option<usize>,
fp16_enable: Option<bool>,
timing_cache: Option<bool>,
timing_cache_path: Option<String>,
force_timing_cache: Option<bool>,
engine_cache: Option<bool>,
engine_cache_path: Option<String>,
dump_ep_context_model: Option<bool>,
ep_context_file_path: Option<String>,
},
CoreML {
ane_only: Option<bool>,
subgraphs: Option<bool>,
},
WebGPU,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OrtSessionConfig {
pub intra_threads: Option<usize>,
pub inter_threads: Option<usize>,
pub parallel_execution: Option<bool>,
pub optimization_level: Option<OrtGraphOptimizationLevel>,
pub execution_providers: Option<Vec<OrtExecutionProvider>>,
pub enable_mem_pattern: Option<bool>,
pub log_severity_level: Option<i32>,
pub log_verbosity_level: Option<i32>,
pub session_config_entries: Option<std::collections::HashMap<String, String>>,
pub arena_shrinkage: Option<bool>,
}
impl OrtSessionConfig {
pub fn new() -> Self {
Self::default()
}
pub fn auto() -> Self {
let mut candidates = vec![
#[cfg(feature = "cuda")]
OrtExecutionProvider::CUDA {
device_id: Some(0),
gpu_mem_limit: None,
arena_extend_strategy: None,
cudnn_conv_algo_search: None,
cudnn_conv_use_max_workspace: None,
},
#[cfg(all(feature = "coreml", any(target_os = "macos", target_os = "ios")))]
OrtExecutionProvider::CoreML {
ane_only: None,
subgraphs: None,
},
#[cfg(all(feature = "directml", target_os = "windows"))]
OrtExecutionProvider::DirectML { device_id: Some(0) },
];
let has_candidates = !candidates.is_empty();
candidates.push(OrtExecutionProvider::CPU);
let config = Self::new().with_execution_providers(candidates);
if has_candidates {
config.with_pending_auto_selection()
} else {
config
}
}
pub(crate) fn with_pending_auto_selection(self) -> Self {
let signature = auto_signature(&self.get_execution_providers());
self.add_config_entry(AUTO_DEVICE_CONFIG_ENTRY, signature)
}
pub fn has_pending_auto_selection(&self) -> bool {
self.session_config_entries
.as_ref()
.and_then(|entries| entries.get(AUTO_DEVICE_CONFIG_ENTRY))
.is_some_and(|value| *value == auto_signature(&self.get_execution_providers()))
}
pub fn resolve_auto(self) -> Self {
self.resolve_auto_with_probe(crate::core::inference::OrtInfer::probe_execution_provider)
}
pub(crate) fn resolve_auto_with_probe(
mut self,
probe: impl FnMut(&OrtExecutionProvider) -> ort::Result<()>,
) -> Self {
let pending = self.has_pending_auto_selection();
self.clear_auto_selection();
if !pending {
return self;
}
let candidates = self
.get_execution_providers()
.into_iter()
.filter(|provider| !matches!(provider, OrtExecutionProvider::CPU))
.collect();
let selected = Self::auto_with_probe(candidates, probe);
let uses_directml = selected
.get_execution_providers()
.iter()
.any(|provider| matches!(provider, OrtExecutionProvider::DirectML { .. }));
self.execution_providers = selected.execution_providers;
if uses_directml {
if self.parallel_execution == Some(true) || self.enable_mem_pattern == Some(true) {
tracing::warn!(
"DirectML was selected automatically; disabling parallel execution and memory patterns"
);
}
self.parallel_execution = Some(false);
self.enable_mem_pattern = Some(false);
}
self
}
fn clear_auto_selection(&mut self) {
if let Some(entries) = self.session_config_entries.as_mut() {
entries.remove(AUTO_DEVICE_CONFIG_ENTRY);
if entries.is_empty() {
self.session_config_entries = None;
}
}
}
fn auto_with_probe(
candidates: Vec<OrtExecutionProvider>,
mut probe: impl FnMut(&OrtExecutionProvider) -> ort::Result<()>,
) -> Self {
let mut providers = Vec::new();
for provider in candidates {
match probe(&provider) {
Ok(()) => providers.push(provider),
Err(error) => {
tracing::debug!(?provider, %error, "automatic execution provider selection failed")
}
}
}
let uses_directml = providers
.iter()
.any(|provider| matches!(provider, OrtExecutionProvider::DirectML { .. }));
tracing::info!(provider = ?providers.first().unwrap_or(&OrtExecutionProvider::CPU), "automatically selected execution provider");
providers.push(OrtExecutionProvider::CPU);
let config = Self::new().with_execution_providers(providers);
if uses_directml {
config
.with_parallel_execution(false)
.with_memory_pattern(false)
} else {
config
}
}
pub fn with_intra_threads(mut self, threads: usize) -> Self {
self.intra_threads = Some(threads);
self
}
pub fn with_inter_threads(mut self, threads: usize) -> Self {
self.inter_threads = Some(threads);
self
}
pub fn with_parallel_execution(mut self, enabled: bool) -> Self {
self.parallel_execution = Some(enabled);
self
}
pub fn with_optimization_level(mut self, level: OrtGraphOptimizationLevel) -> Self {
self.optimization_level = Some(level);
self
}
pub fn with_execution_providers(mut self, providers: Vec<OrtExecutionProvider>) -> Self {
self.clear_auto_selection();
self.execution_providers = Some(providers);
self
}
pub fn add_execution_provider(mut self, provider: OrtExecutionProvider) -> Self {
self.clear_auto_selection();
if let Some(ref mut providers) = self.execution_providers {
providers.push(provider);
} else {
self.execution_providers = Some(vec![provider]);
}
self
}
pub fn with_arena_shrinkage(mut self, enable: bool) -> Self {
self.arena_shrinkage = Some(enable);
self
}
pub fn with_memory_pattern(mut self, enable: bool) -> Self {
self.enable_mem_pattern = Some(enable);
self
}
pub fn with_log_severity_level(mut self, level: i32) -> Self {
self.log_severity_level = Some(level);
self
}
pub fn with_log_verbosity_level(mut self, level: i32) -> Self {
self.log_verbosity_level = Some(level);
self
}
pub fn add_config_entry<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
if let Some(ref mut entries) = self.session_config_entries {
entries.insert(key.into(), value.into());
} else {
let mut entries = std::collections::HashMap::new();
entries.insert(key.into(), value.into());
self.session_config_entries = Some(entries);
}
self
}
pub fn with_coreml_config(mut self, config: OrtCoreMLConfig) -> Self {
let value =
serde_json::to_string(&config).expect("serializing OrtCoreMLConfig cannot fail");
self.session_config_entries
.get_or_insert_with(Default::default)
.insert(COREML_CONFIG_ENTRY.to_owned(), value);
self
}
pub(crate) fn coreml_config(&self) -> Result<Option<OrtCoreMLConfig>, serde_json::Error> {
self.session_config_entries
.as_ref()
.and_then(|entries| entries.get(COREML_CONFIG_ENTRY))
.map(|value| serde_json::from_str(value))
.transpose()
}
pub fn get_intra_threads(&self) -> usize {
self.intra_threads.unwrap_or_else(|| {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
})
}
pub fn get_inter_threads(&self) -> usize {
self.inter_threads.unwrap_or(1)
}
pub fn get_optimization_level(&self) -> OrtGraphOptimizationLevel {
self.optimization_level.unwrap_or_default()
}
pub fn get_execution_providers(&self) -> Vec<OrtExecutionProvider> {
self.execution_providers
.clone()
.unwrap_or_else(|| vec![OrtExecutionProvider::CPU])
}
pub fn has_accelerator_provider(&self) -> bool {
self.execution_providers
.as_ref()
.and_then(|providers| providers.first())
.is_some_and(|provider| !matches!(provider, OrtExecutionProvider::CPU))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn auto_candidates() -> Vec<OrtExecutionProvider> {
vec![
OrtExecutionProvider::CUDA {
device_id: Some(0),
gpu_mem_limit: None,
arena_extend_strategy: None,
cudnn_conv_algo_search: None,
cudnn_conv_use_max_workspace: None,
},
OrtExecutionProvider::CoreML {
ane_only: None,
subgraphs: None,
},
OrtExecutionProvider::DirectML { device_id: Some(0) },
]
}
#[cfg(not(any(feature = "cuda", feature = "coreml", feature = "directml")))]
#[test]
fn auto_without_accelerator_features_is_cpu_only() {
let auto = OrtSessionConfig::auto();
let cpu = OrtSessionConfig::new().with_execution_providers(vec![OrtExecutionProvider::CPU]);
assert_eq!(
serde_json::to_value(auto).unwrap(),
serde_json::to_value(cpu).unwrap()
);
}
#[test]
fn auto_registration_failures_leave_a_cpu_only_configuration() {
let candidates = auto_candidates();
let mut attempted = Vec::new();
let config = OrtSessionConfig::auto_with_probe(candidates.clone(), |provider| {
attempted.push(provider.clone());
Err(ort::Error::new("provider unavailable"))
});
assert_eq!(attempted, candidates);
assert_eq!(
config.get_execution_providers(),
[OrtExecutionProvider::CPU]
);
assert!(!config.has_accelerator_provider());
assert_eq!(config.parallel_execution, None);
assert_eq!(config.enable_mem_pattern, None);
}
#[test]
fn auto_retains_priority_and_configures_directml() {
let candidates = auto_candidates();
let config = OrtSessionConfig::auto_with_probe(candidates.clone(), |_| Ok(()));
let mut expected = candidates;
expected.push(OrtExecutionProvider::CPU);
assert_eq!(config.get_execution_providers(), expected);
assert!(config.has_accelerator_provider());
assert_eq!(config.parallel_execution, Some(false));
assert_eq!(config.enable_mem_pattern, Some(false));
}
#[test]
fn auto_resolution_preserves_tuning_and_removes_internal_marker() {
let mut candidates = auto_candidates();
candidates.push(OrtExecutionProvider::CPU);
let config = OrtSessionConfig::new()
.with_execution_providers(candidates)
.with_pending_auto_selection()
.add_config_entry("session.dynamic_block_base", "4")
.with_intra_threads(2)
.with_parallel_execution(true)
.with_memory_pattern(true);
assert!(config.has_pending_auto_selection());
let resolved = config.resolve_auto_with_probe(|_| Err(ort::Error::new("unavailable")));
assert!(!resolved.has_pending_auto_selection());
assert!(!resolved.has_accelerator_provider());
let expected = OrtSessionConfig::new()
.with_execution_providers(vec![OrtExecutionProvider::CPU])
.add_config_entry("session.dynamic_block_base", "4")
.with_intra_threads(2)
.with_parallel_execution(true)
.with_memory_pattern(true);
assert_eq!(
serde_json::to_value(resolved).unwrap(),
serde_json::to_value(expected).unwrap()
);
}
#[test]
fn auto_resolved_directml_overrides_incompatible_caller_settings() {
let mut candidates = auto_candidates();
candidates.push(OrtExecutionProvider::CPU);
let config = OrtSessionConfig::new()
.with_execution_providers(candidates)
.with_pending_auto_selection()
.with_parallel_execution(true)
.with_memory_pattern(true);
let resolved = config.resolve_auto_with_probe(|provider| {
if matches!(provider, OrtExecutionProvider::DirectML { .. }) {
Ok(())
} else {
Err(ort::Error::new("unavailable"))
}
});
assert_eq!(
resolved.get_execution_providers(),
[
OrtExecutionProvider::DirectML { device_id: Some(0) },
OrtExecutionProvider::CPU,
]
);
assert_eq!(resolved.parallel_execution, Some(false));
assert_eq!(resolved.enable_mem_pattern, Some(false));
}
#[test]
fn direct_provider_replacement_cancels_pending_auto_selection() {
let mut candidates = auto_candidates();
candidates.push(OrtExecutionProvider::CPU);
let mut config = OrtSessionConfig::new()
.with_execution_providers(candidates)
.with_pending_auto_selection();
assert!(config.has_pending_auto_selection());
let explicit = vec![
OrtExecutionProvider::OpenVINO {
device_type: None,
num_threads: None,
},
OrtExecutionProvider::CPU,
];
config.execution_providers = Some(explicit.clone());
assert!(!config.has_pending_auto_selection());
let resolved =
config.resolve_auto_with_probe(|_| panic!("explicit providers must not be probed"));
assert_eq!(resolved.get_execution_providers(), explicit);
assert!(resolved.session_config_entries.is_none());
}
#[test]
fn tuning_a_candidate_keeps_auto_selection_pending() {
let mut config = OrtSessionConfig::new()
.with_execution_providers(auto_candidates())
.with_pending_auto_selection();
if let Some(OrtExecutionProvider::CUDA {
arena_extend_strategy,
..
}) = config
.execution_providers
.as_mut()
.and_then(|eps| eps.first_mut())
{
*arena_extend_strategy = Some("SameAsRequested".to_string());
}
assert!(config.has_pending_auto_selection());
}
#[test]
fn explicit_provider_resolution_never_probes_hardware() {
let config = OrtSessionConfig::new().with_execution_providers(auto_candidates());
let expected = serde_json::to_value(&config).unwrap();
let resolved =
config.resolve_auto_with_probe(|_| panic!("explicit preferences must not be probed"));
assert_eq!(serde_json::to_value(resolved).unwrap(), expected);
}
#[test]
fn explicit_provider_setters_clear_pending_auto_selection() {
let pending = OrtSessionConfig::new()
.with_execution_providers(auto_candidates())
.with_pending_auto_selection();
let explicit = pending
.clone()
.with_execution_providers(vec![OrtExecutionProvider::CPU]);
assert!(!explicit.has_pending_auto_selection());
assert_eq!(
explicit.get_execution_providers(),
[OrtExecutionProvider::CPU]
);
let appended = pending.add_execution_provider(OrtExecutionProvider::CPU);
assert!(!appended.has_pending_auto_selection());
let mut expected = auto_candidates();
expected.push(OrtExecutionProvider::CPU);
assert_eq!(appended.get_execution_providers(), expected);
}
#[test]
fn auto_skips_an_unavailable_cuda_provider() {
let config = OrtSessionConfig::auto_with_probe(auto_candidates(), |provider| {
if matches!(provider, OrtExecutionProvider::CUDA { .. }) {
Err(ort::Error::new("CUDA driver unavailable"))
} else {
Ok(())
}
});
assert_eq!(
config.get_execution_providers(),
[
OrtExecutionProvider::CoreML {
ane_only: None,
subgraphs: None
},
OrtExecutionProvider::DirectML { device_id: Some(0) },
OrtExecutionProvider::CPU,
]
);
}
#[test]
fn test_ort_session_config_builder() {
let config = OrtSessionConfig::new()
.with_intra_threads(4)
.with_inter_threads(2)
.with_optimization_level(OrtGraphOptimizationLevel::Level2)
.with_memory_pattern(true)
.add_execution_provider(OrtExecutionProvider::CPU);
assert_eq!(config.intra_threads, Some(4));
assert_eq!(config.inter_threads, Some(2));
assert!(matches!(
config.optimization_level,
Some(OrtGraphOptimizationLevel::Level2)
));
assert_eq!(config.enable_mem_pattern, Some(true));
assert!(config.execution_providers.is_some());
}
#[test]
fn test_ort_session_config_getters() {
let config = OrtSessionConfig::new()
.with_intra_threads(8)
.with_inter_threads(4)
.with_optimization_level(OrtGraphOptimizationLevel::All);
assert_eq!(config.get_intra_threads(), 8);
assert_eq!(config.get_inter_threads(), 4);
assert!(matches!(
config.get_optimization_level(),
OrtGraphOptimizationLevel::All
));
}
#[test]
fn test_accelerator_provider_detection() {
assert!(!OrtSessionConfig::new().has_accelerator_provider());
assert!(
!OrtSessionConfig::new()
.with_execution_providers(vec![OrtExecutionProvider::CPU])
.has_accelerator_provider()
);
assert!(
OrtSessionConfig::new()
.with_execution_providers(vec![
OrtExecutionProvider::DirectML { device_id: Some(0) },
OrtExecutionProvider::CPU,
])
.has_accelerator_provider()
);
assert!(
!OrtSessionConfig::new()
.with_execution_providers(vec![
OrtExecutionProvider::CPU,
OrtExecutionProvider::DirectML { device_id: Some(0) },
])
.has_accelerator_provider()
);
}
#[test]
fn coreml_provider_keeps_legacy_variant_shape() {
let provider = OrtExecutionProvider::CoreML {
ane_only: Some(true),
subgraphs: Some(false),
};
let OrtExecutionProvider::CoreML {
ane_only,
subgraphs,
} = provider
else {
unreachable!()
};
assert_eq!(ane_only, Some(true));
assert_eq!(subgraphs, Some(false));
}
#[test]
fn coreml_advanced_config_round_trips_through_session_config() {
let expected = OrtCoreMLConfig {
compute_units: Some(OrtCoreMLComputeUnits::CPUAndGPU),
model_format: Some(OrtCoreMLModelFormat::MLProgram),
static_input_shapes: Some(true),
specialization_strategy: Some(OrtCoreMLSpecializationStrategy::FastPrediction),
allow_low_precision_accumulation_on_gpu: Some(true),
profile_compute_plan: None,
model_cache_dir: Some("cache".to_owned()),
};
let config = OrtSessionConfig::new().with_coreml_config(expected.clone());
assert_eq!(config.coreml_config().unwrap(), Some(expected));
}
}