#![allow(clippy::result_large_err)]
#![allow(clippy::too_many_arguments)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use onnx_runtime_ir::{DataType, DeviceType, Shape};
use onnx_runtime_tracer::{Args, SpanGuard};
pub use epcontext::{
CompiledPartition, EpContextPlacement, dump_session_ep_context, load_ep_context_nodes,
};
pub use error::SessionError;
pub use executor::{
CacheStats, CaptureDecline, CaptureDeclineReport, CapturePathKind, ControlFlowStats,
DeviceAllocationCounts, DeviceGraphCaptureResult, ExecutionProviderDecline,
ExecutionProviderFallbackReport, SeamReason, exec_phase_stats, print_exec_phase_profile,
};
pub use onnx_runtime_loader::{
EpContextDumpConfig, EpContextPartition, Model as EncoderModel, ModelMetadata,
};
pub use tensor::{DeviceBindingTransferStats, DeviceIoBinding, Tensor, cpu_allocator};
mod epcontext;
mod executor;
mod fp16_decode;
pub mod sequence;
mod tensor;
fn trace_span(name: &'static str, cat: &'static str) -> Option<SpanGuard> {
onnx_runtime_tracer::global_context()
.filter(|trace| trace.is_enabled())
.map(|trace| trace.span(name, cat))
}
#[derive(Debug)]
pub enum SessionOutput {
Tensor(Tensor),
Sequence(sequence::SequenceValue),
}
impl SessionOutput {
pub fn as_tensor(&self) -> Option<&Tensor> {
match self {
Self::Tensor(tensor) => Some(tensor),
Self::Sequence(_) => None,
}
}
pub fn as_sequence(&self) -> Option<&sequence::SequenceValue> {
match self {
Self::Tensor(_) => None,
Self::Sequence(sequence) => Some(sequence),
}
}
pub fn into_tensor(self) -> Option<Tensor> {
match self {
Self::Tensor(tensor) => Some(tensor),
Self::Sequence(_) => None,
}
}
pub fn into_sequence(self) -> Option<sequence::SequenceValue> {
match self {
Self::Tensor(_) => None,
Self::Sequence(sequence) => Some(sequence),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OpsetVersion {
Known(u64),
Undeclared,
}
impl std::fmt::Display for OpsetVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Known(version) => version.fmt(f),
Self::Undeclared => f.write_str("<undeclared>"),
}
}
}
mod error {
use super::OpsetVersion;
struct UnsupportedOpRemediation<'a> {
opset: OpsetVersion,
domain: &'a str,
}
impl std::fmt::Display for UnsupportedOpRemediation<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.opset == OpsetVersion::Undeclared {
write!(
f,
"declare an opset_import for domain {:?} in the model, ",
self.domain
)?;
}
f.write_str(
"enable another EP that supports this operator and opset, convert or decompose \
the model operator, or file an nxrt issue with the model details",
)
}
}
fn unsupported_op_remediation(
opset: OpsetVersion,
domain: &str,
) -> UnsupportedOpRemediation<'_> {
UnsupportedOpRemediation { opset, domain }
}
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error("session not initialized")]
NotInitialized,
#[error("input not found: {name}")]
InputNotFound { name: String },
#[error("unknown session option: {key}")]
UnknownOption { key: String },
#[error("invalid value {value:?} for session option {key:?}: expected one of {expected}")]
InvalidOption {
key: String,
value: String,
expected: String,
},
#[error("no model source: set a path or bytes on the builder")]
NoModelSource,
#[error("execution provider unavailable: {0}")]
ExecutionProviderUnavailable(String),
#[error(
"CUDA execution required by ONNX_GENAI_REQUIRE_CUDA=1, but CPU fallback is needed: \
{unsupported_nodes}"
)]
HeterogeneousPlacementRequired { unsupported_nodes: String },
#[error(
"unsupported operator {domain}::{op_type}: no available execution provider has a \
kernel; node {node}, opset {opset}; decline reason: {reason}; consulted execution \
providers (priority order): {execution_providers}. To fix: {remediation}",
remediation = unsupported_op_remediation(*.opset, .domain)
)]
UnsupportedOp {
op_type: String,
domain: String,
node: String,
opset: OpsetVersion,
reason: String,
execution_providers: String,
},
#[error("value has a non-static (symbolic) shape and no binding to resolve it: {value}")]
DynamicShape { value: String },
#[error(
"symbol {symbol} bound to conflicting sizes {first} and {second} across bound inputs"
)]
SymbolConflict {
symbol: String,
first: usize,
second: usize,
},
#[error("input {name}: rank mismatch (graph declares rank {expected}, got {got})")]
RankMismatch {
name: String,
expected: usize,
got: usize,
},
#[error("no inferred shape for value {value} produced by op {op}")]
UnresolvedShape { value: String, op: String },
#[error("shape element count overflows usize for value {value} (dims {dims:?})")]
ShapeOverflow { value: String, dims: Vec<usize> },
#[error(
"op {op} produced {got} data-dependent output shape(s) but has {expected} output(s)"
)]
OutputShapeCountMismatch {
op: String,
expected: usize,
got: usize,
},
#[error(
"runtime broadcast shape resolution failed for node {node} ({domain}::{op_type}): \
concrete input shapes {input_shapes:?} are not broadcast-compatible, so no valid \
elementwise output shape exists. To fix: update the model or runtime inputs so each \
aligned dimension is equal or one of them is 1"
)]
RuntimeBroadcastIncompatible {
node: String,
domain: String,
op_type: String,
input_shapes: Vec<Vec<usize>>,
},
#[error("input {name}: dtype mismatch (expected {expected}, got {got})")]
DtypeMismatch {
name: String,
expected: String,
got: String,
},
#[error("input {name}: shape mismatch (expected {expected:?}, got {got:?})")]
ShapeMismatch {
name: String,
expected: Vec<usize>,
got: Vec<usize>,
},
#[error("internal executor error: {0}")]
Internal(String),
#[error("Sequence op {op}: {reason}")]
SequenceOp { op: String, reason: String },
#[error("control-flow op {op}: {reason}")]
ControlFlow { op: String, reason: String },
#[error(
"EPContext reference node (main_context=0) has no matching primary \
(source={source_key:?}, partition_name={partition_name:?})"
)]
DanglingEpContext {
source_key: Option<String>,
partition_name: Option<String>,
},
#[error(transparent)]
Load(#[from] onnx_runtime_loader::LoaderError),
#[error(transparent)]
Ep(#[from] onnx_runtime_ep_api::EpError),
#[error(transparent)]
Ir(#[from] onnx_runtime_ir::IrError),
#[error(transparent)]
Graph(#[from] onnx_runtime_ir::GraphError),
#[error(transparent)]
Optimize(#[from] onnx_runtime_optimizer::OptimizerError),
#[error(transparent)]
ShapeInfer(#[from] onnx_runtime_shape_inference::ShapeInferError),
}
impl SessionError {
pub(crate) fn unsupported_op(
node: &onnx_runtime_ir::Node,
node_id: onnx_runtime_ir::NodeId,
opset: u64,
execution_providers: impl Into<String>,
reason: impl Into<String>,
) -> Self {
let domain = if node.domain.is_empty() {
"ai.onnx".to_string()
} else {
node.domain.clone()
};
let node_display = if node.name.is_empty() {
format!("<unnamed node #{}>", node_id.0)
} else {
format!("{:?}", node.name)
};
let opset = if opset == u64::MAX {
OpsetVersion::Undeclared
} else {
OpsetVersion::Known(opset)
};
Self::UnsupportedOp {
op_type: node.op_type.clone(),
domain,
node: node_display,
opset,
reason: reason.into(),
execution_providers: execution_providers.into(),
}
}
}
pub type Result<T> = std::result::Result<T, SessionError>;
}
use error::Result;
#[derive(Clone, Debug)]
pub struct IoMeta {
pub name: String,
pub dtype: DataType,
pub shape: Shape,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum DevicePreference {
#[default]
Auto,
Cpu,
Gpu { index: Option<u32> },
Explicit { device_type: DeviceType, index: u32 },
}
#[derive(Clone, Debug)]
pub struct WarmupShape {
pub input_name: String,
pub shape: Vec<usize>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DecodePrecision {
#[default]
Model,
Fp16,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OptimizationLevel {
#[default]
None,
Basic,
All,
}
impl OptimizationLevel {
fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"none" | "off" | "0" => Some(Self::None),
"basic" => Some(Self::Basic),
"all" => Some(Self::All),
_ => None,
}
}
fn passes(self) -> Vec<Box<dyn onnx_runtime_optimizer::OptimizationPass>> {
use onnx_runtime_optimizer::{ConstantFolding, DeadNodeElimination, OpFusion};
match self {
Self::None => Vec::new(),
Self::Basic => vec![Box::new(ConstantFolding), Box::new(DeadNodeElimination)],
Self::All => vec![
Box::new(ConstantFolding),
Box::new(DeadNodeElimination),
Box::new(OpFusion::new()),
],
}
}
}
#[derive(Default)]
pub struct SessionBuilder {
model_path: Option<PathBuf>,
model_bytes: Option<Vec<u8>>,
device: DevicePreference,
execution_provider: Option<std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>>,
memory_limit: Option<usize>,
enable_profiling: bool,
warmup_shapes: Vec<WarmupShape>,
decode_precision: DecodePrecision,
options: HashMap<String, String>,
}
impl SessionBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn model(mut self, path: impl AsRef<Path>) -> Self {
self.model_path = Some(path.as_ref().to_path_buf());
self
}
pub fn model_bytes(mut self, bytes: &[u8]) -> Self {
self.model_bytes = Some(bytes.to_vec());
self
}
pub fn device(mut self, pref: DevicePreference) -> Self {
self.device = pref;
self
}
pub fn execution_provider(
mut self,
execution_provider: std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>,
) -> Self {
self.execution_provider = Some(execution_provider);
self
}
pub fn memory_limit(mut self, bytes: usize) -> Self {
self.memory_limit = Some(bytes);
self
}
pub fn profiling(mut self, enable: bool) -> Self {
self.enable_profiling = enable;
self
}
pub fn warmup(mut self, shapes: Vec<WarmupShape>) -> Self {
self.warmup_shapes = shapes;
self
}
pub fn decode_precision(mut self, precision: DecodePrecision) -> Self {
self.decode_precision = precision;
self
}
pub fn option(mut self, key: &str, value: &str) -> Self {
self.options.insert(key.to_string(), value.to_string());
self
}
fn parse_options(
options: &HashMap<String, String>,
) -> Result<(OptimizationLevel, EpContextDumpConfig)> {
let mut level = OptimizationLevel::None;
let mut ctx = EpContextDumpConfig::default();
for (key, value) in options {
match key.as_str() {
"optimization" => {
level = OptimizationLevel::parse(value).ok_or_else(|| {
SessionError::InvalidOption {
key: key.clone(),
value: value.clone(),
expected: "none, basic, all".to_string(),
}
})?;
}
"ep.context_enable" => {
ctx.enable = parse_bool_option(key, value)?;
}
"ep.context_file_path" => {
ctx.file_path = if value.trim().is_empty() {
None
} else {
Some(PathBuf::from(value))
};
}
"ep.context_embed_mode" => {
ctx.embed_mode = parse_embed_mode(key, value)?;
}
_ => return Err(SessionError::UnknownOption { key: key.clone() }),
}
}
Ok((level, ctx))
}
pub fn build(self) -> Result<InferenceSession> {
let (level, ep_context_config) = Self::parse_options(&self.options)?;
let _ = (self.memory_limit, self.enable_profiling);
let (mut graph, weights, model_dir, model_metadata) =
match (self.model_path, self.model_bytes) {
(Some(path), _) => {
let model_dir = path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let bytes = onnx_runtime_loader::read_model_binary(&path)?;
let metadata = {
let mut span = trace_span("load.model_metadata", "load");
let metadata = model_metadata_from_bytes(&bytes)?;
if let Some(span) = span.as_mut() {
span.set_args(
Args::new()
.bytes(bytes.len() as u64)
.with("metadata_props", metadata.metadata_props.len() as u64),
);
}
metadata
};
let (g, w) =
onnx_runtime_loader::load_model_bytes_with_weights(&bytes, &model_dir)?;
(g, w, model_dir, metadata)
}
(None, Some(bytes)) => {
let metadata = {
let mut span = trace_span("load.model_metadata", "load");
let metadata = model_metadata_from_bytes(&bytes)?;
if let Some(span) = span.as_mut() {
span.set_args(
Args::new()
.bytes(bytes.len() as u64)
.with("metadata_props", metadata.metadata_props.len() as u64),
);
}
metadata
};
let (g, w) = onnx_runtime_loader::load_model_bytes_with_weights(&bytes, ".")?;
(g, w, PathBuf::from("."), metadata)
}
(None, None) => return Err(SessionError::NoModelSource),
};
{
let nodes_before = graph.num_nodes();
let mut span = trace_span("session.fp16_decode", "session");
fp16_decode::maybe_convert_decode_fp16(
&mut graph,
&weights,
self.decode_precision,
device_preference_is_gpu(&self.device),
);
if let Some(span) = span.as_mut() {
span.set_args(
Args::new()
.with("nodes_before", nodes_before as u64)
.with("nodes_after", graph.num_nodes() as u64)
.with("device_gpu", device_preference_is_gpu(&self.device)),
);
}
}
optimize_graph(&mut graph, level)?;
let ep = {
let mut span = trace_span("session.select_execution_provider", "session");
let ep = match self.execution_provider {
Some(ep) => ep,
None => select_execution_provider(&self.device)?,
};
if let Some(span) = span.as_mut() {
span.set_args(
Args::new()
.with("provider", ep.name().to_string())
.with("device", ep.device_type().trace_name().into_owned())
.with("device_index", ep.device_id().index as u64),
);
}
ep
};
let mut session = InferenceSession::from_parts(
graph,
weights,
&model_dir,
ep_context_config,
model_metadata,
ep,
)?;
if !self.warmup_shapes.is_empty() {
let mut span = trace_span("session.warmup", "session");
session.warmup(&self.warmup_shapes)?;
if let Some(span) = span.as_mut() {
span.set_args(Args::new().with("shape_count", self.warmup_shapes.len() as u64));
}
}
Ok(session)
}
}
fn device_preference_is_gpu(preference: &DevicePreference) -> bool {
match preference {
DevicePreference::Gpu { .. } => true,
DevicePreference::Explicit { device_type, .. } => !device_type.is_host_accessible(),
DevicePreference::Auto | DevicePreference::Cpu => false,
}
}
fn select_execution_provider(
preference: &DevicePreference,
) -> Result<std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>> {
match preference {
DevicePreference::Auto | DevicePreference::Cpu => executor::auto_detect_cpu_ep(),
DevicePreference::Explicit {
device_type: DeviceType::Cpu,
index: 0,
} => executor::auto_detect_cpu_ep(),
DevicePreference::Gpu { index } => cuda_execution_provider(index.unwrap_or(0)),
DevicePreference::Explicit {
device_type: DeviceType::Cuda,
index,
} => cuda_execution_provider(*index),
DevicePreference::Explicit { device_type, index } => {
Err(SessionError::ExecutionProviderUnavailable(format!(
"{device_type:?}:{index} is not implemented by onnx-runtime-session"
)))
}
}
}
#[cfg(feature = "cuda")]
fn cuda_execution_provider(
index: u32,
) -> Result<std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>> {
let mut ep = onnx_runtime_ep_cuda::CudaExecutionProvider::new(index)?;
onnx_runtime_ep_api::ExecutionProvider::initialize(&mut ep, &Default::default())?;
Ok(std::sync::Arc::new(ep))
}
#[cfg(not(feature = "cuda"))]
fn cuda_execution_provider(
index: u32,
) -> Result<std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>> {
Err(SessionError::ExecutionProviderUnavailable(format!(
"CUDA:{index} requested, but onnx-runtime-session was built without the `cuda` feature"
)))
}
fn model_metadata_from_bytes(bytes: &[u8]) -> Result<ModelMetadata> {
let model = onnx_runtime_loader::proto::decode_model(bytes)?;
Ok(ModelMetadata {
ir_version: model.ir_version,
producer_name: model.producer_name,
producer_version: model.producer_version,
domain: model.domain,
model_version: model.model_version,
doc_string: (!model.doc_string.is_empty()).then_some(model.doc_string),
graph_name: model.graph.map(|graph| graph.name).unwrap_or_default(),
metadata_props: model
.metadata_props
.into_iter()
.map(|entry| (entry.key, entry.value))
.collect(),
})
}
fn parse_bool_option(key: &str, value: &str) -> Result<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" => Ok(true),
"0" | "false" => Ok(false),
_ => Err(SessionError::InvalidOption {
key: key.to_string(),
value: value.to_string(),
expected: "0, 1, true, false".to_string(),
}),
}
}
fn parse_embed_mode(key: &str, value: &str) -> Result<u8> {
match value.trim() {
"0" => Ok(0),
"1" => Ok(1),
_ => Err(SessionError::InvalidOption {
key: key.to_string(),
value: value.to_string(),
expected: "0, 1".to_string(),
}),
}
}
fn optimize_graph(graph: &mut onnx_runtime_ir::Graph, level: OptimizationLevel) -> Result<()> {
let passes = level.passes();
if passes.is_empty() {
return Ok(());
}
{
let nodes_before = graph.num_nodes();
let mut span = trace_span("session.optimize_graph", "session");
onnx_runtime_optimizer::run_passes(
graph,
&passes,
&onnx_runtime_optimizer::PassContext::new(),
)?;
if let Some(span) = span.as_mut() {
span.set_args(
Args::new()
.with("passes", passes.len() as u64)
.with("nodes_before", nodes_before as u64)
.with("nodes_after_passes", graph.num_nodes() as u64),
);
}
}
graph
.opset_imports
.entry(onnx_runtime_optimizer::CONTRIB_DOMAIN.to_string())
.or_insert(1);
let registry = onnx_runtime_shape_inference::InferenceRegistry::default_registry();
let opset_imports = graph.opset_imports.clone();
{
let mut span = trace_span("session.optimize_shape_inference", "session");
registry.infer_graph(
graph,
&opset_imports,
onnx_runtime_shape_inference::MergePolicy::Permissive,
)?;
if let Some(span) = span.as_mut() {
span.set_args(
Args::new()
.with("nodes", graph.num_nodes() as u64)
.with("opset_domains", opset_imports.len() as u64),
);
}
}
Ok(())
}
pub struct InferenceSession {
inputs: Vec<IoMeta>,
outputs: Vec<IoMeta>,
model_metadata: ModelMetadata,
exec: executor::Executor,
ep_context_config: EpContextDumpConfig,
}
fn io_meta(graph: &onnx_runtime_ir::Graph, values: &[onnx_runtime_ir::ValueId]) -> Vec<IoMeta> {
values
.iter()
.map(|&vid| {
let v = graph.value(vid);
IoMeta {
name: v.name.clone().unwrap_or_default(),
dtype: v.dtype,
shape: v.shape.clone(),
}
})
.collect()
}
impl InferenceSession {
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
Self::builder().model(path).build()
}
pub fn load_bytes(bytes: &[u8]) -> Result<Self> {
Self::builder().model_bytes(bytes).build()
}
pub fn from_graph(graph: onnx_runtime_ir::Graph) -> Result<Self> {
Self::from_parts(
graph,
std::sync::Arc::new(onnx_runtime_loader::WeightStore::new()),
Path::new("."),
EpContextDumpConfig::default(),
ModelMetadata::default(),
executor::auto_detect_cpu_ep()?,
)
}
fn from_parts(
graph: onnx_runtime_ir::Graph,
weights: std::sync::Arc<onnx_runtime_loader::WeightStore>,
model_dir: &Path,
ep_context_config: EpContextDumpConfig,
model_metadata: ModelMetadata,
ep: std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>,
) -> Result<Self> {
let mut graph = graph;
{
let mut span = trace_span("session.normalize_validate", "session");
graph.normalize_domains();
onnx_runtime_loader::validate_model(&graph)?;
if let Some(span) = span.as_mut() {
span.set_args(
Args::new()
.with("nodes", graph.num_nodes() as u64)
.with("values", graph.values.len() as u64),
);
}
}
let (inputs, outputs) = {
let mut span = trace_span("session.io_meta", "session");
let inputs = io_meta(&graph, &graph.inputs);
let outputs = io_meta(&graph, &graph.outputs);
if let Some(span) = span.as_mut() {
span.set_args(
Args::new()
.with("inputs", inputs.len() as u64)
.with("outputs", outputs.len() as u64),
);
}
(inputs, outputs)
};
let eps: [(
onnx_runtime_ep_api::EpId,
&dyn onnx_runtime_ep_api::ExecutionProvider,
); 1] = [(onnx_runtime_ep_api::EpId(0), ep.as_ref())];
{
let mut span = trace_span("session.epcontext_restore", "session");
epcontext::load_ep_context_nodes(&graph, model_dir, &eps)?;
if let Some(span) = span.as_mut() {
span.set_args(
Args::new()
.with("provider", ep.name().to_string())
.with("model_dir", model_dir.display().to_string()),
);
}
}
let exec = {
let mut span = trace_span("session.executor_build", "session");
let exec = executor::Executor::build(graph, weights, ep)?;
if let Some(span) = span.as_mut() {
span.set_args(Args::new().with("cache_entries", exec.cache_stats().entries as u64));
}
exec
};
Ok(Self {
inputs,
outputs,
model_metadata,
exec,
ep_context_config,
})
}
pub fn builder() -> SessionBuilder {
SessionBuilder::new()
}
pub fn run(&mut self, inputs: &[(&str, &Tensor)]) -> Result<Vec<Tensor>> {
self.exec.run(inputs)
}
pub fn set_trace_context(&mut self, trace: onnx_runtime_tracer::TraceContext) {
self.exec.set_trace_context(trace);
}
pub fn run_outputs(&mut self, inputs: &[(&str, &Tensor)]) -> Result<Vec<SessionOutput>> {
self.exec.run_outputs(inputs)
}
pub fn decode_memo_counts(&self) -> (u64, u64, u64, u64) {
self.exec.decode_memo_counts()
}
pub fn decode_view_plan_counts(&self) -> (u64, u64) {
self.exec.decode_view_plan_counts()
}
pub fn run_with_device_bindings(
&mut self,
inputs: &[(&str, &Tensor)],
bindings: &mut [DeviceIoBinding],
) -> Result<Vec<Option<Tensor>>> {
self.exec.run_with_device_bindings(inputs, bindings)
}
pub fn allocate_device_binding(
&self,
input_name: impl Into<String>,
output_name: Option<impl Into<String>>,
dtype: DataType,
physical_shape: Vec<usize>,
logical_shape: Vec<usize>,
) -> Result<DeviceIoBinding> {
self.exec.allocate_device_binding(
input_name.into(),
output_name.map(Into::into),
dtype,
physical_shape,
logical_shape,
)
}
pub fn allocate_device_output_binding(
&self,
output_name: impl Into<String>,
dtype: DataType,
physical_shape: Vec<usize>,
logical_shape: Vec<usize>,
) -> Result<DeviceIoBinding> {
self.exec.allocate_device_output_binding(
output_name.into(),
dtype,
physical_shape,
logical_shape,
)
}
pub fn try_capture_with_device_bindings(
&mut self,
inputs: &[(&str, &Tensor)],
bindings: &mut [DeviceIoBinding],
) -> Result<DeviceGraphCaptureResult> {
self.exec.try_capture_with_device_bindings(inputs, bindings)
}
pub fn replay_device_graph(&mut self, bindings: &mut [DeviceIoBinding]) -> Result<bool> {
self.exec.replay_device_graph(bindings)
}
pub fn reset_device_graph(&mut self) -> Result<bool> {
self.exec.reset_device_graph()
}
pub fn captured_graph_segment_count(&self) -> usize {
self.exec.captured_segment_count()
}
pub fn capture_segmentation(&self) -> &[CaptureDecline] {
self.exec.capture_segmentation()
}
pub fn check_device_capture_error(&self) -> Result<u32> {
self.exec.check_device_capture_error()
}
pub fn device_allocation_counts(&self) -> Option<DeviceAllocationCounts> {
self.exec.device_allocation_counts()
}
pub fn device_id(&self) -> onnx_runtime_ir::DeviceId {
self.exec.device_id()
}
pub fn execution_provider_fallback_report(&self) -> Option<&ExecutionProviderFallbackReport> {
self.exec.execution_provider_fallback_report()
}
pub fn inputs(&self) -> &[IoMeta] {
&self.inputs
}
pub fn outputs(&self) -> &[IoMeta] {
&self.outputs
}
pub fn model_metadata(&self) -> &ModelMetadata {
&self.model_metadata
}
pub fn cache_stats(&self) -> CacheStats {
self.exec.cache_stats()
}
pub fn control_flow_stats(&self) -> ControlFlowStats {
self.exec.control_flow_stats()
}
pub fn warmup(&mut self, shapes: &[WarmupShape]) -> Result<()> {
for ws in shapes {
if !self.inputs.iter().any(|m| m.name == ws.input_name) {
return Err(SessionError::InputNotFound {
name: ws.input_name.clone(),
});
}
}
self.exec.warmup()
}
pub fn ep_context_config(&self) -> &EpContextDumpConfig {
&self.ep_context_config
}
pub fn graph(&self) -> &onnx_runtime_ir::Graph {
self.exec.graph()
}
pub fn export_ep_context(
&self,
orig_path: &Path,
partitions: &[CompiledPartition],
) -> Result<PathBuf> {
let model = EncoderModel::new(self.exec.graph()).with_weights(self.exec.weights().as_ref());
dump_session_ep_context(&model, orig_path, partitions, &self.ep_context_config)
}
}
pub fn load(path: impl AsRef<Path>) -> Result<InferenceSession> {
InferenceSession::load(path)
}
#[cfg(test)]
mod device_binding_tests {
use super::*;
#[cfg(feature = "cuda")]
use onnx_runtime_ir::Attribute;
#[cfg(feature = "cuda")]
use onnx_runtime_ir::static_shape;
use onnx_runtime_ir::{Graph, Node, NodeId};
#[test]
fn persistent_binding_aliases_input_output_and_suppresses_materialization() {
let mut graph = Graph::new();
graph.opset_imports.insert("".into(), 13);
let length = graph.intern_symbol("length");
let input = graph.create_named_value("input", DataType::Float32, vec![length.into()]);
graph.add_input(input);
let output = graph.create_named_value("output", DataType::Float32, vec![length.into()]);
graph.insert_node(Node::new(
NodeId(0),
"Relu",
vec![Some(input)],
vec![output],
));
graph.add_output(output);
let mut session = InferenceSession::from_graph(graph).unwrap();
let mut binding = session
.allocate_device_binding("input", Some("output"), DataType::Float32, vec![4], vec![2])
.unwrap();
let ptr = binding.device_ptr();
let bytes = [-2.0f32, 3.0, -4.0, 5.0]
.into_iter()
.flat_map(f32::to_le_bytes)
.collect::<Vec<_>>();
binding.write_bytes(0, &bytes).unwrap();
let outputs = session
.run_with_device_bindings(&[], std::slice::from_mut(&mut binding))
.unwrap();
assert_eq!(outputs.len(), 1);
assert!(outputs[0].is_none());
assert_eq!(binding.device_ptr(), ptr);
assert_eq!(binding.logical_shape(), &[2]);
let values = binding
.read_bytes()
.unwrap()
.chunks_exact(4)
.map(|bytes| f32::from_le_bytes(bytes.try_into().unwrap()))
.collect::<Vec<_>>();
assert_eq!(values, vec![0.0, 3.0, -4.0, 5.0]);
assert_eq!(
binding.transfer_stats(),
DeviceBindingTransferStats {
host_upload_calls: 1,
host_upload_bytes: 16,
host_download_calls: 1,
host_download_bytes: 16,
}
);
}
#[cfg(feature = "cuda")]
#[test]
fn cuda_graph_replay_uses_persistent_io_without_device_allocations() {
let Ok(mut ep) = onnx_runtime_ep_cuda::CudaExecutionProvider::new(0) else {
eprintln!("skipping session CUDA graph test: CUDA runtime unavailable");
return;
};
onnx_runtime_ep_api::ExecutionProvider::initialize(&mut ep, &Default::default()).unwrap();
let mut graph = Graph::new();
graph.opset_imports.insert("".into(), 13);
let input = graph.create_named_value("input", DataType::Int64, static_shape([1]));
graph.add_input(input);
let output = graph.create_named_value("output", DataType::Float32, static_shape([1]));
let mut cast = Node::new(NodeId(0), "Cast", vec![Some(input)], vec![output]);
cast.attributes
.insert("to".into(), Attribute::Int(DataType::Float32 as i64));
graph.insert_node(cast);
graph.add_output(output);
let mut session = InferenceSession::from_parts(
graph,
std::sync::Arc::new(onnx_runtime_loader::WeightStore::new()),
Path::new("."),
EpContextDumpConfig::default(),
ModelMetadata::default(),
std::sync::Arc::new(ep),
)
.unwrap();
let mut input = session
.allocate_device_binding("input", None::<String>, DataType::Int64, vec![1], vec![1])
.unwrap();
let output = session
.allocate_device_output_binding("output", DataType::Float32, vec![1], vec![1])
.unwrap();
input.write_bytes(0, &7i64.to_le_bytes()).unwrap();
let mut bindings = vec![input, output];
session
.run_with_device_bindings(&[], &mut bindings)
.unwrap();
input_write(&mut bindings[0], 11);
assert!(matches!(
session
.try_capture_with_device_bindings(&[], &mut bindings)
.unwrap(),
DeviceGraphCaptureResult::Captured(_)
));
assert_eq!(read_bound_f32(&mut bindings[1]), 11.0);
let before = session.device_allocation_counts().unwrap();
input_write(&mut bindings[0], 23);
session.replay_device_graph(&mut bindings).unwrap();
assert_eq!(read_bound_f32(&mut bindings[1]), 23.0);
assert_eq!(session.device_allocation_counts().unwrap(), before);
assert!(session.reset_device_graph().unwrap());
input_write(&mut bindings[0], 31);
assert!(matches!(
session
.try_capture_with_device_bindings(&[], &mut bindings)
.unwrap(),
DeviceGraphCaptureResult::Captured(_)
));
assert_eq!(read_bound_f32(&mut bindings[1]), 31.0);
input_write(&mut bindings[0], 47);
session.replay_device_graph(&mut bindings).unwrap();
assert_eq!(read_bound_f32(&mut bindings[1]), 47.0);
assert!(session.reset_device_graph().unwrap());
}
#[cfg(feature = "cuda")]
#[test]
fn segmented_cuda_graph_claims_whole_subgraph_around_eager_seam() {
let Ok(mut ep) = onnx_runtime_ep_cuda::CudaExecutionProvider::new(0) else {
eprintln!("skipping segmented session CUDA graph test: CUDA runtime unavailable");
return;
};
onnx_runtime_ep_api::ExecutionProvider::initialize(&mut ep, &Default::default()).unwrap();
let n = 4usize;
let mut graph = Graph::new();
graph.opset_imports.insert("".into(), 13);
let input = graph.create_named_value("input", DataType::Int64, static_shape([n]));
graph.add_input(input);
let as_float = graph.create_named_value("as_float", DataType::Float32, static_shape([n]));
let mut cast_in = Node::new(NodeId(0), "Cast", vec![Some(input)], vec![as_float]);
cast_in
.attributes
.insert("to".into(), Attribute::Int(DataType::Float32 as i64));
graph.insert_node(cast_in);
let clipped = graph.create_named_value("clipped", DataType::Float32, static_shape([n]));
let mut clip = Node::new(NodeId(1), "Clip", vec![Some(as_float)], vec![clipped]);
clip.attributes
.insert("min".into(), Attribute::Float(-1000.0));
clip.attributes
.insert("max".into(), Attribute::Float(1000.0));
graph.insert_node(clip);
let output = graph.create_named_value("output", DataType::Int64, static_shape([n]));
let mut cast_out = Node::new(NodeId(2), "Cast", vec![Some(clipped)], vec![output]);
cast_out
.attributes
.insert("to".into(), Attribute::Int(DataType::Int64 as i64));
graph.insert_node(cast_out);
graph.add_output(output);
let mut session = InferenceSession::from_parts(
graph,
std::sync::Arc::new(onnx_runtime_loader::WeightStore::new()),
Path::new("."),
EpContextDumpConfig::default(),
ModelMetadata::default(),
std::sync::Arc::new(ep),
)
.unwrap();
let input_binding = session
.allocate_device_binding("input", None::<String>, DataType::Int64, vec![n], vec![n])
.unwrap();
let output_binding = session
.allocate_device_output_binding("output", DataType::Int64, vec![n], vec![n])
.unwrap();
let mut bindings = vec![input_binding, output_binding];
let values_a = [-2i64, 3, -4, 5];
write_bound_i64(&mut bindings[0], &values_a);
session
.run_with_device_bindings(&[], &mut bindings)
.unwrap();
let eager_a = read_bound_i64_vec(&mut bindings[1]);
assert_eq!(
eager_a,
values_a.to_vec(),
"Cast∘Clip∘Cast round-trips ints"
);
match session
.try_capture_with_device_bindings(&[], &mut bindings)
.unwrap()
{
DeviceGraphCaptureResult::Captured(outputs) => {
assert!(
outputs.iter().all(Option::is_none),
"device-bound outputs must not materialize to host"
);
}
DeviceGraphCaptureResult::NotCapturable(report) => {
panic!(
"expected the CUDA EP to claim the whole subgraph via segmented capture, \
got a full decline: {report}"
);
}
}
assert_eq!(
session.captured_graph_segment_count(),
2,
"Clip should split the plan into two captured Cast segments"
);
let seams = session.capture_segmentation();
assert_eq!(seams.len(), 1, "exactly one eager seam node (Clip)");
assert_eq!(seams[0].op_type, "Clip");
assert_eq!(read_bound_i64_vec(&mut bindings[1]), eager_a);
let values_b = [7i64, -1, 0, -8];
write_bound_i64(&mut bindings[0], &values_b);
session.replay_device_graph(&mut bindings).unwrap();
let replay_b = read_bound_i64_vec(&mut bindings[1]);
assert!(session.reset_device_graph().unwrap());
write_bound_i64(&mut bindings[0], &values_b);
session
.run_with_device_bindings(&[], &mut bindings)
.unwrap();
let eager_b = read_bound_i64_vec(&mut bindings[1]);
assert_eq!(
replay_b, eager_b,
"segmented replay must be bit-identical to eager execution"
);
assert_eq!(replay_b, values_b.to_vec());
}
#[cfg(feature = "cuda")]
fn write_bound_i64(binding: &mut DeviceIoBinding, values: &[i64]) {
let bytes = values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect::<Vec<_>>();
binding.write_bytes(0, &bytes).unwrap();
}
#[cfg(feature = "cuda")]
fn read_bound_i64_vec(binding: &mut DeviceIoBinding) -> Vec<i64> {
binding
.read_bytes()
.unwrap()
.chunks_exact(8)
.map(|bytes| i64::from_le_bytes(bytes.try_into().unwrap()))
.collect()
}
#[cfg(feature = "cuda")]
fn input_write(binding: &mut DeviceIoBinding, value: i64) {
binding.write_bytes(0, &value.to_le_bytes()).unwrap();
}
#[cfg(feature = "cuda")]
fn read_bound_f32(binding: &mut DeviceIoBinding) -> f32 {
let bytes = binding.read_bytes().unwrap();
f32::from_le_bytes(bytes.try_into().unwrap())
}
}
#[cfg(test)]
mod option_tests {
use super::*;
fn opts(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
fn level_of(pairs: &[(&str, &str)]) -> Result<OptimizationLevel> {
SessionBuilder::parse_options(&opts(pairs)).map(|(level, _)| level)
}
fn ctx_of(pairs: &[(&str, &str)]) -> Result<EpContextDumpConfig> {
SessionBuilder::parse_options(&opts(pairs)).map(|(_, ctx)| ctx)
}
#[test]
fn optimization_defaults_to_none_when_unset() {
assert_eq!(level_of(&[]).unwrap(), OptimizationLevel::None);
}
#[test]
fn explicit_execution_provider_is_retained_by_builder() {
let builder =
SessionBuilder::new().execution_provider(executor::auto_detect_cpu_ep().unwrap());
assert!(builder.execution_provider.is_some());
}
#[test]
fn optimization_parses_known_values() {
for (v, want) in [
("none", OptimizationLevel::None),
("off", OptimizationLevel::None),
("BASIC", OptimizationLevel::Basic),
("All", OptimizationLevel::All),
] {
assert_eq!(
level_of(&[("optimization", v)]).unwrap(),
want,
"value {v:?}"
);
}
}
#[test]
fn unknown_option_key_is_rejected() {
let err = level_of(&[("optimisation", "all")]).unwrap_err();
assert!(matches!(err, SessionError::UnknownOption { key } if key == "optimisation"));
}
#[test]
fn invalid_optimization_value_is_rejected() {
let err = level_of(&[("optimization", "aggressive")]).unwrap_err();
assert!(matches!(
err,
SessionError::InvalidOption { key, value, .. } if key == "optimization" && value == "aggressive"
));
}
#[test]
fn none_level_selects_no_passes() {
assert!(OptimizationLevel::None.passes().is_empty());
assert_eq!(OptimizationLevel::Basic.passes().len(), 2);
assert_eq!(OptimizationLevel::All.passes().len(), 3);
}
#[test]
fn ep_context_defaults_to_disabled() {
let ctx = ctx_of(&[]).unwrap();
assert_eq!(ctx, EpContextDumpConfig::default());
assert!(!ctx.enable);
assert_eq!(ctx.file_path, None);
assert_eq!(ctx.embed_mode, 1);
}
#[test]
fn ep_context_enable_parses_bool_forms() {
for (v, want) in [
("1", true),
("0", false),
("true", true),
("TRUE", true),
("false", false),
("False", false),
] {
let ctx = ctx_of(&[("ep.context_enable", v)]).unwrap();
assert_eq!(ctx.enable, want, "value {v:?}");
}
}
#[test]
fn ep_context_enable_rejects_garbage() {
let err = ctx_of(&[("ep.context_enable", "yes")]).unwrap_err();
assert!(matches!(
err,
SessionError::InvalidOption { key, value, .. }
if key == "ep.context_enable" && value == "yes"
));
}
#[test]
fn ep_context_file_path_parses_and_empty_clears() {
let ctx = ctx_of(&[("ep.context_file_path", "/out/net_ctx.onnx")]).unwrap();
assert_eq!(ctx.file_path, Some(PathBuf::from("/out/net_ctx.onnx")));
let ctx = ctx_of(&[("ep.context_file_path", "")]).unwrap();
assert_eq!(ctx.file_path, None);
}
#[test]
fn ep_context_embed_mode_parses_and_rejects() {
assert_eq!(
ctx_of(&[("ep.context_embed_mode", "0")])
.unwrap()
.embed_mode,
0
);
assert_eq!(
ctx_of(&[("ep.context_embed_mode", "1")])
.unwrap()
.embed_mode,
1
);
let err = ctx_of(&[("ep.context_embed_mode", "2")]).unwrap_err();
assert!(matches!(
err,
SessionError::InvalidOption { key, value, expected }
if key == "ep.context_embed_mode" && value == "2" && expected == "0, 1"
));
}
#[test]
fn ep_context_options_combine_with_optimization() {
let (level, ctx) = SessionBuilder::parse_options(&opts(&[
("optimization", "all"),
("ep.context_enable", "1"),
("ep.context_file_path", "/tmp/out_ctx.onnx"),
("ep.context_embed_mode", "0"),
]))
.unwrap();
assert_eq!(level, OptimizationLevel::All);
assert!(ctx.enable);
assert_eq!(ctx.file_path, Some(PathBuf::from("/tmp/out_ctx.onnx")));
assert_eq!(ctx.embed_mode, 0);
}
}