use serde::Serialize;
use crate::config::{Accelerator, Backend, ModelConfig};
use crate::error::Result;
#[cfg(any(feature = "ort", test))]
const RELEASE_BRANCH_PREFIX: &str = "rel-";
#[cfg(any(feature = "ort", test))]
const VERSION_SEGMENTS: usize = 3;
#[derive(Debug, Clone, Serialize)]
pub struct RuntimeInfo {
pub sceptre_version: &'static str,
pub os: &'static str,
pub arch: &'static str,
pub backend: Backend,
pub accelerator_requested: Accelerator,
pub accelerator_registered: Option<Accelerator>,
pub ort: Option<OrtRuntimeInfo>,
}
#[derive(Debug, Clone, Serialize)]
pub struct OrtRuntimeInfo {
pub provisioning: &'static str,
pub dylib_path: Option<String>,
pub build_info: String,
pub version: Option<String>,
}
pub fn runtime_info() -> Result<RuntimeInfo> {
runtime_info_for(&ModelConfig::default())
}
pub fn runtime_info_for(model: &ModelConfig) -> Result<RuntimeInfo> {
model.validate()?;
let (accelerator_registered, ort) = match model.backend {
Backend::Ort => probe_ort(model.accelerator),
Backend::Tract => (Some(Accelerator::Cpu), None),
Backend::Candle => (probe_candle(model.accelerator), None),
};
Ok(RuntimeInfo {
sceptre_version: crate::VERSION,
os: std::env::consts::OS,
arch: std::env::consts::ARCH,
backend: model.backend,
accelerator_requested: model.accelerator,
accelerator_registered,
ort,
})
}
#[cfg(any(feature = "ort", test))]
const fn ort_provisioning() -> &'static str {
if cfg!(feature = "ort-bundled") {
"bundled"
} else if cfg!(feature = "ort-dynamic") {
"dynamic"
} else {
"system"
}
}
#[cfg(feature = "ort")]
fn probe_ort(requested: Accelerator) -> (Option<Accelerator>, Option<OrtRuntimeInfo>) {
let probed = std::panic::catch_unwind(|| {
let builder = ort::session::Session::builder().ok()?;
let build_info = ort::info().to_string();
let registered = super::ort_ep::apply_accelerator(builder, requested)
.map(|(_builder, registered)| registered)
.ok();
Some((registered, build_info))
});
match probed {
Ok(Some((registered, build_info))) => {
let version = parse_ort_version(&build_info);
(
registered,
Some(OrtRuntimeInfo {
provisioning: ort_provisioning(),
dylib_path: std::env::var("ORT_DYLIB_PATH").ok(),
build_info,
version,
}),
)
}
Ok(None) => (None, None),
Err(_) => {
tracing::warn!("the ONNX Runtime native library could not be loaded; runtime details are unavailable");
(None, None)
}
}
}
#[cfg(not(feature = "ort"))]
fn probe_ort(_requested: Accelerator) -> (Option<Accelerator>, Option<OrtRuntimeInfo>) {
(None, None)
}
#[cfg(feature = "candle")]
fn probe_candle(requested: Accelerator) -> Option<Accelerator> {
super::candle::probe_accelerator(requested)
}
#[cfg(not(feature = "candle"))]
fn probe_candle(_requested: Accelerator) -> Option<Accelerator> {
None
}
#[cfg(any(feature = "ort", test))]
fn parse_ort_version(build_info: &str) -> Option<String> {
let start = build_info.find(RELEASE_BRANCH_PREFIX)? + RELEASE_BRANCH_PREFIX.len();
let rest = &build_info[start..];
let end = rest
.find(|character: char| !character.is_ascii_digit() && character != '.')
.unwrap_or(rest.len());
let candidate = &rest[..end];
let segments: Vec<&str> = candidate.split('.').collect();
let numeric = segments.len() == VERSION_SEGMENTS
&& segments
.iter()
.all(|segment| !segment.is_empty() && segment.bytes().all(|byte| byte.is_ascii_digit()));
numeric.then(|| candidate.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_parse_the_release_version_from_a_build_info_string() {
let build_info = "ORT Build Info: git-branch=rel-1.28.0, git-commit-id=26250ae, build type=Release";
assert_eq!(parse_ort_version(build_info).as_deref(), Some("1.28.0"));
}
#[test]
fn should_parse_a_release_version_at_the_end_of_the_string() {
assert_eq!(parse_ort_version("git-branch=rel-1.19.2").as_deref(), Some("1.19.2"));
}
#[test]
fn should_not_parse_a_version_from_a_non_release_branch() {
assert_eq!(
parse_ort_version("ORT Build Info: git-branch=main, build type=Release"),
None
);
}
#[test]
fn should_not_parse_a_partial_or_malformed_version() {
assert_eq!(parse_ort_version("git-branch=rel-1.28,"), None);
assert_eq!(parse_ort_version("git-branch=rel-1.28."), None);
assert_eq!(parse_ort_version("git-branch=rel-"), None);
assert_eq!(parse_ort_version("git-branch=rel-1.2.3.4"), None);
}
#[test]
fn should_not_report_the_c_api_level_as_the_runtime_version() {
assert_eq!(parse_ort_version("ORT Build Info: api-18, git-branch=dev"), None);
}
#[test]
fn should_report_a_cpu_only_backend_as_running_on_the_cpu() {
let model = ModelConfig {
backend: Backend::Tract,
accelerator: Accelerator::Auto,
..ModelConfig::default()
};
let info = runtime_info_for(&model).expect("auto on tract is a valid configuration");
assert_eq!(info.backend, Backend::Tract);
assert_eq!(info.accelerator_requested, Accelerator::Auto);
assert_eq!(info.accelerator_registered, Some(Accelerator::Cpu));
assert!(info.ort.is_none(), "the tract backend has no ONNX Runtime details");
assert_eq!(info.sceptre_version, crate::VERSION);
assert_eq!(info.os, std::env::consts::OS);
assert_eq!(info.arch, std::env::consts::ARCH);
}
#[cfg(feature = "candle")]
#[test]
fn should_report_the_device_the_candle_backend_would_open() {
let model = ModelConfig {
backend: Backend::Candle,
accelerator: Accelerator::Auto,
..ModelConfig::default()
};
let info = runtime_info_for(&model).expect("auto on candle is a valid configuration");
let registered = info.accelerator_registered.expect("auto always resolves");
assert_ne!(registered, Accelerator::Auto, "auto must resolve to a real device");
assert!(
Backend::Candle.supports(registered),
"candle reported {registered:?}, which it cannot run on"
);
assert!(info.ort.is_none(), "the candle backend has no ONNX Runtime details");
}
#[cfg(all(feature = "candle-metal", target_os = "macos"))]
#[test]
fn should_resolve_auto_to_metal_on_a_mac_that_compiled_it_in() {
let model = ModelConfig {
backend: Backend::Candle,
accelerator: Accelerator::Auto,
..ModelConfig::default()
};
let info = runtime_info_for(&model).expect("auto on candle is a valid configuration");
assert_eq!(info.accelerator_registered, Some(Accelerator::Metal));
}
#[cfg(not(feature = "candle"))]
#[test]
fn should_not_claim_a_device_for_a_backend_that_is_not_compiled_in() {
let model = ModelConfig {
backend: Backend::Candle,
..ModelConfig::default()
};
let info = runtime_info_for(&model).expect("cpu on candle is a valid configuration");
assert_eq!(
info.accelerator_registered, None,
"an absent backend must report an undetermined accelerator"
);
}
#[test]
fn should_reject_an_invalid_configuration() {
let model = ModelConfig {
backend: Backend::Tract,
accelerator: Accelerator::Cuda,
..ModelConfig::default()
};
runtime_info_for(&model).expect_err("cuda on tract must not be described as runnable");
}
#[test]
fn should_serialize_runtime_info_as_json() {
let model = ModelConfig {
backend: Backend::Tract,
..ModelConfig::default()
};
let info = runtime_info_for(&model).expect("the default tract configuration is valid");
let json: serde_json::Value = serde_json::to_value(&info).expect("serialize the runtime info");
assert_eq!(json["backend"], "tract");
assert_eq!(json["accelerator_requested"], "cpu");
assert_eq!(json["accelerator_registered"], "cpu");
assert_eq!(json["sceptre_version"], crate::VERSION);
assert!(json["ort"].is_null());
}
#[test]
fn should_name_the_provisioning_mode() {
assert!(
matches!(ort_provisioning(), "bundled" | "dynamic" | "system"),
"unexpected provisioning mode {}",
ort_provisioning()
);
}
#[cfg(feature = "ort-bundled")]
#[test]
fn should_report_the_bundled_onnx_runtime_build() {
let info = runtime_info().expect("the default configuration is valid");
assert_eq!(info.backend, Backend::Ort);
assert_eq!(info.accelerator_requested, Accelerator::Cpu);
assert_eq!(info.accelerator_registered, Some(Accelerator::Cpu));
let ort = info.ort.expect("the bundled ONNX Runtime must be loadable");
assert_eq!(ort.provisioning, "bundled");
assert!(
ort.build_info.contains("ORT Build Info"),
"unexpected build info: {}",
ort.build_info
);
assert_eq!(
ort.version,
parse_ort_version(&ort.build_info),
"the reported version must come from the build info string"
);
}
#[cfg(feature = "ort-bundled")]
#[test]
fn should_resolve_auto_to_a_concrete_accelerator_on_ort() {
let model = ModelConfig {
backend: Backend::Ort,
accelerator: Accelerator::Auto,
..ModelConfig::default()
};
let info = runtime_info_for(&model).expect("auto on ort is a valid configuration");
let registered = info.accelerator_registered.expect("auto always resolves");
assert_ne!(registered, Accelerator::Auto, "auto must resolve to a real device");
}
}