1#![allow(clippy::result_large_err)]
20#![allow(clippy::too_many_arguments)]
22
23use std::collections::HashMap;
24use std::path::{Path, PathBuf};
25
26use onnx_runtime_ir::{DataType, DeviceType, Shape};
27
28pub use epcontext::{
29 CompiledPartition, EpContextPlacement, dump_session_ep_context, load_ep_context_nodes,
30};
31pub use error::SessionError;
32pub use executor::{
33 CacheStats, CaptureDecline, CaptureDeclineReport, CapturePathKind, ControlFlowStats,
34 DeviceAllocationCounts, DeviceGraphCaptureResult, ExecutionProviderDecline,
35 ExecutionProviderFallbackReport, SeamReason, print_exec_phase_profile,
36};
37pub use onnx_runtime_loader::{
38 EpContextDumpConfig, EpContextPartition, Model as EncoderModel, ModelMetadata,
39};
40pub use tensor::{DeviceBindingTransferStats, DeviceIoBinding, Tensor, cpu_allocator};
41
42mod epcontext;
43mod executor;
44mod fp16_decode;
45pub mod sequence;
46mod tensor;
47
48#[derive(Debug)]
50pub enum SessionOutput {
51 Tensor(Tensor),
52 Sequence(sequence::SequenceValue),
53}
54
55impl SessionOutput {
56 pub fn as_tensor(&self) -> Option<&Tensor> {
57 match self {
58 Self::Tensor(tensor) => Some(tensor),
59 Self::Sequence(_) => None,
60 }
61 }
62
63 pub fn as_sequence(&self) -> Option<&sequence::SequenceValue> {
64 match self {
65 Self::Tensor(_) => None,
66 Self::Sequence(sequence) => Some(sequence),
67 }
68 }
69
70 pub fn into_tensor(self) -> Option<Tensor> {
71 match self {
72 Self::Tensor(tensor) => Some(tensor),
73 Self::Sequence(_) => None,
74 }
75 }
76
77 pub fn into_sequence(self) -> Option<sequence::SequenceValue> {
78 match self {
79 Self::Tensor(_) => None,
80 Self::Sequence(sequence) => Some(sequence),
81 }
82 }
83}
84
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum OpsetVersion {
88 Known(u64),
90 Undeclared,
92}
93
94impl std::fmt::Display for OpsetVersion {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self {
97 Self::Known(version) => version.fmt(f),
98 Self::Undeclared => f.write_str("<undeclared>"),
99 }
100 }
101}
102
103mod error {
104 use super::OpsetVersion;
105
106 struct UnsupportedOpRemediation<'a> {
107 opset: OpsetVersion,
108 domain: &'a str,
109 }
110
111 impl std::fmt::Display for UnsupportedOpRemediation<'_> {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 if self.opset == OpsetVersion::Undeclared {
114 write!(
115 f,
116 "declare an opset_import for domain {:?} in the model, ",
117 self.domain
118 )?;
119 }
120 f.write_str(
121 "enable another EP that supports this operator and opset, convert or decompose \
122 the model operator, or file an nxrt issue with the model details",
123 )
124 }
125 }
126
127 fn unsupported_op_remediation(
128 opset: OpsetVersion,
129 domain: &str,
130 ) -> UnsupportedOpRemediation<'_> {
131 UnsupportedOpRemediation { opset, domain }
132 }
133
134 #[derive(Debug, thiserror::Error)]
136 pub enum SessionError {
137 #[error("session not initialized")]
138 NotInitialized,
139
140 #[error("input not found: {name}")]
141 InputNotFound { name: String },
142
143 #[error("unknown session option: {key}")]
144 UnknownOption { key: String },
145
146 #[error("invalid value {value:?} for session option {key:?}: expected one of {expected}")]
147 InvalidOption {
148 key: String,
149 value: String,
150 expected: String,
151 },
152
153 #[error("no model source: set a path or bytes on the builder")]
154 NoModelSource,
155
156 #[error("execution provider unavailable: {0}")]
157 ExecutionProviderUnavailable(String),
158
159 #[error(
160 "CUDA execution required by ONNX_GENAI_REQUIRE_CUDA=1, but CPU fallback is needed: \
161 {unsupported_nodes}"
162 )]
163 HeterogeneousPlacementRequired { unsupported_nodes: String },
164
165 #[error(
166 "unsupported operator {domain}::{op_type}: no available execution provider has a \
167 kernel; node {node}, opset {opset}; decline reason: {reason}; consulted execution \
168 providers (priority order): {execution_providers}. To fix: {remediation}",
169 remediation = unsupported_op_remediation(*.opset, .domain)
170 )]
171 UnsupportedOp {
172 op_type: String,
173 domain: String,
174 node: String,
175 opset: OpsetVersion,
176 reason: String,
177 execution_providers: String,
178 },
179
180 #[error("value has a non-static (symbolic) shape and no binding to resolve it: {value}")]
181 DynamicShape { value: String },
182
183 #[error(
184 "symbol {symbol} bound to conflicting sizes {first} and {second} across bound inputs"
185 )]
186 SymbolConflict {
187 symbol: String,
188 first: usize,
189 second: usize,
190 },
191
192 #[error("input {name}: rank mismatch (graph declares rank {expected}, got {got})")]
193 RankMismatch {
194 name: String,
195 expected: usize,
196 got: usize,
197 },
198
199 #[error("no inferred shape for value {value} produced by op {op}")]
200 UnresolvedShape { value: String, op: String },
201
202 #[error("shape element count overflows usize for value {value} (dims {dims:?})")]
203 ShapeOverflow { value: String, dims: Vec<usize> },
204
205 #[error(
206 "op {op} produced {got} data-dependent output shape(s) but has {expected} output(s)"
207 )]
208 OutputShapeCountMismatch {
209 op: String,
210 expected: usize,
211 got: usize,
212 },
213
214 #[error(
215 "runtime broadcast shape resolution failed for node {node} ({domain}::{op_type}): \
216 concrete input shapes {input_shapes:?} are not broadcast-compatible, so no valid \
217 elementwise output shape exists. To fix: update the model or runtime inputs so each \
218 aligned dimension is equal or one of them is 1"
219 )]
220 RuntimeBroadcastIncompatible {
221 node: String,
222 domain: String,
223 op_type: String,
224 input_shapes: Vec<Vec<usize>>,
225 },
226
227 #[error("input {name}: dtype mismatch (expected {expected}, got {got})")]
228 DtypeMismatch {
229 name: String,
230 expected: String,
231 got: String,
232 },
233
234 #[error("input {name}: shape mismatch (expected {expected:?}, got {got:?})")]
235 ShapeMismatch {
236 name: String,
237 expected: Vec<usize>,
238 got: Vec<usize>,
239 },
240
241 #[error("internal executor error: {0}")]
242 Internal(String),
243
244 #[error("Sequence op {op}: {reason}")]
245 SequenceOp { op: String, reason: String },
246
247 #[error("control-flow op {op}: {reason}")]
248 ControlFlow { op: String, reason: String },
249
250 #[error(
251 "EPContext reference node (main_context=0) has no matching primary \
252 (source={source_key:?}, partition_name={partition_name:?})"
253 )]
254 DanglingEpContext {
255 source_key: Option<String>,
256 partition_name: Option<String>,
257 },
258
259 #[error(transparent)]
260 Load(#[from] onnx_runtime_loader::LoaderError),
261
262 #[error(transparent)]
263 Ep(#[from] onnx_runtime_ep_api::EpError),
264
265 #[error(transparent)]
266 Ir(#[from] onnx_runtime_ir::IrError),
267
268 #[error(transparent)]
269 Graph(#[from] onnx_runtime_ir::GraphError),
270
271 #[error(transparent)]
272 Optimize(#[from] onnx_runtime_optimizer::OptimizerError),
273
274 #[error(transparent)]
275 ShapeInfer(#[from] onnx_runtime_shape_inference::ShapeInferError),
276 }
277
278 impl SessionError {
279 pub(crate) fn unsupported_op(
280 node: &onnx_runtime_ir::Node,
281 node_id: onnx_runtime_ir::NodeId,
282 opset: u64,
283 execution_providers: impl Into<String>,
284 reason: impl Into<String>,
285 ) -> Self {
286 let domain = if node.domain.is_empty() {
287 "ai.onnx".to_string()
288 } else {
289 node.domain.clone()
290 };
291 let node_display = if node.name.is_empty() {
292 format!("<unnamed node #{}>", node_id.0)
293 } else {
294 format!("{:?}", node.name)
295 };
296 let opset = if opset == u64::MAX {
297 OpsetVersion::Undeclared
298 } else {
299 OpsetVersion::Known(opset)
300 };
301 Self::UnsupportedOp {
302 op_type: node.op_type.clone(),
303 domain,
304 node: node_display,
305 opset,
306 reason: reason.into(),
307 execution_providers: execution_providers.into(),
308 }
309 }
310 }
311
312 pub type Result<T> = std::result::Result<T, SessionError>;
314}
315
316use error::Result;
317
318#[derive(Clone, Debug)]
320pub struct IoMeta {
321 pub name: String,
322 pub dtype: DataType,
323 pub shape: Shape,
324}
325
326#[derive(Clone, Debug, Default, PartialEq, Eq)]
329pub enum DevicePreference {
330 #[default]
332 Auto,
333 Cpu,
335 Gpu { index: Option<u32> },
337 Explicit { device_type: DeviceType, index: u32 },
339}
340
341#[derive(Clone, Debug)]
343pub struct WarmupShape {
344 pub input_name: String,
345 pub shape: Vec<usize>,
346}
347
348#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
361pub enum DecodePrecision {
362 #[default]
364 Model,
365 Fp16,
369}
370
371#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
383pub enum OptimizationLevel {
384 #[default]
386 None,
387 Basic,
391 All,
395}
396
397impl OptimizationLevel {
398 fn parse(value: &str) -> Option<Self> {
401 match value.trim().to_ascii_lowercase().as_str() {
402 "none" | "off" | "0" => Some(Self::None),
403 "basic" => Some(Self::Basic),
404 "all" => Some(Self::All),
405 _ => None,
406 }
407 }
408
409 fn passes(self) -> Vec<Box<dyn onnx_runtime_optimizer::OptimizationPass>> {
412 use onnx_runtime_optimizer::{ConstantFolding, DeadNodeElimination, OpFusion};
413 match self {
414 Self::None => Vec::new(),
415 Self::Basic => vec![Box::new(ConstantFolding), Box::new(DeadNodeElimination)],
416 Self::All => vec![
417 Box::new(ConstantFolding),
418 Box::new(DeadNodeElimination),
419 Box::new(OpFusion::new()),
420 ],
421 }
422 }
423}
424
425#[derive(Default)]
427pub struct SessionBuilder {
428 model_path: Option<PathBuf>,
429 model_bytes: Option<Vec<u8>>,
430 device: DevicePreference,
431 execution_provider: Option<std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>>,
432 memory_limit: Option<usize>,
433 enable_profiling: bool,
434 warmup_shapes: Vec<WarmupShape>,
435 decode_precision: DecodePrecision,
436 options: HashMap<String, String>,
437}
438
439impl SessionBuilder {
440 pub fn new() -> Self {
441 Self::default()
442 }
443
444 pub fn model(mut self, path: impl AsRef<Path>) -> Self {
445 self.model_path = Some(path.as_ref().to_path_buf());
446 self
447 }
448
449 pub fn model_bytes(mut self, bytes: &[u8]) -> Self {
450 self.model_bytes = Some(bytes.to_vec());
451 self
452 }
453
454 pub fn device(mut self, pref: DevicePreference) -> Self {
455 self.device = pref;
456 self
457 }
458
459 pub fn execution_provider(
461 mut self,
462 execution_provider: std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>,
463 ) -> Self {
464 self.execution_provider = Some(execution_provider);
465 self
466 }
467
468 pub fn memory_limit(mut self, bytes: usize) -> Self {
469 self.memory_limit = Some(bytes);
470 self
471 }
472
473 pub fn profiling(mut self, enable: bool) -> Self {
474 self.enable_profiling = enable;
475 self
476 }
477
478 pub fn warmup(mut self, shapes: Vec<WarmupShape>) -> Self {
479 self.warmup_shapes = shapes;
480 self
481 }
482
483 pub fn decode_precision(mut self, precision: DecodePrecision) -> Self {
488 self.decode_precision = precision;
489 self
490 }
491
492 pub fn option(mut self, key: &str, value: &str) -> Self {
511 self.options.insert(key.to_string(), value.to_string());
512 self
513 }
514
515 fn parse_options(
531 options: &HashMap<String, String>,
532 ) -> Result<(OptimizationLevel, EpContextDumpConfig)> {
533 let mut level = OptimizationLevel::None;
534 let mut ctx = EpContextDumpConfig::default();
535 for (key, value) in options {
536 match key.as_str() {
537 "optimization" => {
538 level = OptimizationLevel::parse(value).ok_or_else(|| {
539 SessionError::InvalidOption {
540 key: key.clone(),
541 value: value.clone(),
542 expected: "none, basic, all".to_string(),
543 }
544 })?;
545 }
546 "ep.context_enable" => {
547 ctx.enable = parse_bool_option(key, value)?;
548 }
549 "ep.context_file_path" => {
550 ctx.file_path = if value.trim().is_empty() {
552 None
553 } else {
554 Some(PathBuf::from(value))
555 };
556 }
557 "ep.context_embed_mode" => {
558 ctx.embed_mode = parse_embed_mode(key, value)?;
559 }
560 _ => return Err(SessionError::UnknownOption { key: key.clone() }),
563 }
564 }
565 Ok((level, ctx))
566 }
567
568 pub fn build(self) -> Result<InferenceSession> {
591 let (level, ep_context_config) = Self::parse_options(&self.options)?;
592
593 let _ = (self.memory_limit, self.enable_profiling);
595
596 let (mut graph, weights, model_dir, model_metadata) =
597 match (self.model_path, self.model_bytes) {
598 (Some(path), _) => {
599 let model_dir = path
603 .parent()
604 .map(Path::to_path_buf)
605 .unwrap_or_else(|| PathBuf::from("."));
606 let bytes = onnx_runtime_loader::read_model_binary(&path)?;
607 let metadata = model_metadata_from_bytes(&bytes)?;
608 let (g, w) =
609 onnx_runtime_loader::load_model_bytes_with_weights(&bytes, &model_dir)?;
610 (g, w, model_dir, metadata)
611 }
612 (None, Some(bytes)) => {
613 let metadata = model_metadata_from_bytes(&bytes)?;
614 let (g, w) = onnx_runtime_loader::load_model_bytes_with_weights(&bytes, ".")?;
615 (g, w, PathBuf::from("."), metadata)
616 }
617 (None, None) => return Err(SessionError::NoModelSource),
618 };
619
620 fp16_decode::maybe_convert_decode_fp16(
628 &mut graph,
629 &weights,
630 self.decode_precision,
631 device_preference_is_gpu(&self.device),
632 );
633
634 optimize_graph(&mut graph, level)?;
636 let ep = match self.execution_provider {
637 Some(ep) => ep,
638 None => select_execution_provider(&self.device)?,
639 };
640
641 let mut session = InferenceSession::from_parts(
642 graph,
643 weights,
644 &model_dir,
645 ep_context_config,
646 model_metadata,
647 ep,
648 )?;
649 if !self.warmup_shapes.is_empty() {
650 session.warmup(&self.warmup_shapes)?;
651 }
652 Ok(session)
653 }
654}
655
656fn device_preference_is_gpu(preference: &DevicePreference) -> bool {
659 match preference {
660 DevicePreference::Gpu { .. } => true,
661 DevicePreference::Explicit { device_type, .. } => !device_type.is_host_accessible(),
662 DevicePreference::Auto | DevicePreference::Cpu => false,
663 }
664}
665
666fn select_execution_provider(
667 preference: &DevicePreference,
668) -> Result<std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>> {
669 match preference {
670 DevicePreference::Auto | DevicePreference::Cpu => executor::auto_detect_cpu_ep(),
673 DevicePreference::Explicit {
674 device_type: DeviceType::Cpu,
675 index: 0,
676 } => executor::auto_detect_cpu_ep(),
677 DevicePreference::Gpu { index } => cuda_execution_provider(index.unwrap_or(0)),
678 DevicePreference::Explicit {
679 device_type: DeviceType::Cuda,
680 index,
681 } => cuda_execution_provider(*index),
682 DevicePreference::Explicit { device_type, index } => {
683 Err(SessionError::ExecutionProviderUnavailable(format!(
684 "{device_type:?}:{index} is not implemented by onnx-runtime-session"
685 )))
686 }
687 }
688}
689
690#[cfg(feature = "cuda")]
691fn cuda_execution_provider(
692 index: u32,
693) -> Result<std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>> {
694 let mut ep = onnx_runtime_ep_cuda::CudaExecutionProvider::new(index)?;
695 onnx_runtime_ep_api::ExecutionProvider::initialize(&mut ep, &Default::default())?;
696 Ok(std::sync::Arc::new(ep))
697}
698
699#[cfg(not(feature = "cuda"))]
700fn cuda_execution_provider(
701 index: u32,
702) -> Result<std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>> {
703 Err(SessionError::ExecutionProviderUnavailable(format!(
704 "CUDA:{index} requested, but onnx-runtime-session was built without the `cuda` feature"
705 )))
706}
707
708fn model_metadata_from_bytes(bytes: &[u8]) -> Result<ModelMetadata> {
709 let model = onnx_runtime_loader::proto::decode_model(bytes)?;
710 Ok(ModelMetadata {
711 ir_version: model.ir_version,
712 producer_name: model.producer_name,
713 producer_version: model.producer_version,
714 domain: model.domain,
715 model_version: model.model_version,
716 doc_string: (!model.doc_string.is_empty()).then_some(model.doc_string),
717 graph_name: model.graph.map(|graph| graph.name).unwrap_or_default(),
718 metadata_props: model
719 .metadata_props
720 .into_iter()
721 .map(|entry| (entry.key, entry.value))
722 .collect(),
723 })
724}
725
726fn parse_bool_option(key: &str, value: &str) -> Result<bool> {
731 match value.trim().to_ascii_lowercase().as_str() {
732 "1" | "true" => Ok(true),
733 "0" | "false" => Ok(false),
734 _ => Err(SessionError::InvalidOption {
735 key: key.to_string(),
736 value: value.to_string(),
737 expected: "0, 1, true, false".to_string(),
738 }),
739 }
740}
741
742fn parse_embed_mode(key: &str, value: &str) -> Result<u8> {
747 match value.trim() {
748 "0" => Ok(0),
749 "1" => Ok(1),
750 _ => Err(SessionError::InvalidOption {
751 key: key.to_string(),
752 value: value.to_string(),
753 expected: "0, 1".to_string(),
754 }),
755 }
756}
757
758fn optimize_graph(graph: &mut onnx_runtime_ir::Graph, level: OptimizationLevel) -> Result<()> {
765 let passes = level.passes();
766 if passes.is_empty() {
767 return Ok(());
768 }
769
770 onnx_runtime_optimizer::run_passes(
771 graph,
772 &passes,
773 &onnx_runtime_optimizer::PassContext::new(),
774 )?;
775
776 graph
781 .opset_imports
782 .entry(onnx_runtime_optimizer::CONTRIB_DOMAIN.to_string())
783 .or_insert(1);
784
785 let registry = onnx_runtime_shape_inference::InferenceRegistry::default_registry();
788 let opset_imports = graph.opset_imports.clone();
789 registry.infer_graph(
790 graph,
791 &opset_imports,
792 onnx_runtime_shape_inference::MergePolicy::Permissive,
793 )?;
794
795 Ok(())
796}
797
798pub struct InferenceSession {
800 inputs: Vec<IoMeta>,
801 outputs: Vec<IoMeta>,
802 model_metadata: ModelMetadata,
803 exec: executor::Executor,
804 ep_context_config: EpContextDumpConfig,
808}
809
810fn io_meta(graph: &onnx_runtime_ir::Graph, values: &[onnx_runtime_ir::ValueId]) -> Vec<IoMeta> {
811 values
812 .iter()
813 .map(|&vid| {
814 let v = graph.value(vid);
815 IoMeta {
816 name: v.name.clone().unwrap_or_default(),
817 dtype: v.dtype,
818 shape: v.shape.clone(),
819 }
820 })
821 .collect()
822}
823
824impl InferenceSession {
825 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
827 Self::builder().model(path).build()
828 }
829
830 pub fn load_bytes(bytes: &[u8]) -> Result<Self> {
832 Self::builder().model_bytes(bytes).build()
833 }
834
835 pub fn from_graph(graph: onnx_runtime_ir::Graph) -> Result<Self> {
841 Self::from_parts(
845 graph,
846 std::sync::Arc::new(onnx_runtime_loader::WeightStore::new()),
847 Path::new("."),
848 EpContextDumpConfig::default(),
849 ModelMetadata::default(),
850 executor::auto_detect_cpu_ep()?,
851 )
852 }
853
854 fn from_parts(
855 graph: onnx_runtime_ir::Graph,
856 weights: std::sync::Arc<onnx_runtime_loader::WeightStore>,
857 model_dir: &Path,
858 ep_context_config: EpContextDumpConfig,
859 model_metadata: ModelMetadata,
860 ep: std::sync::Arc<dyn onnx_runtime_ep_api::ExecutionProvider>,
861 ) -> Result<Self> {
862 let mut graph = graph;
867 graph.normalize_domains();
868
869 onnx_runtime_loader::validate_model(&graph)?;
870
871 let inputs = io_meta(&graph, &graph.inputs);
872 let outputs = io_meta(&graph, &graph.outputs);
873 let eps: [(
880 onnx_runtime_ep_api::EpId,
881 &dyn onnx_runtime_ep_api::ExecutionProvider,
882 ); 1] = [(onnx_runtime_ep_api::EpId(0), ep.as_ref())];
883 epcontext::load_ep_context_nodes(&graph, model_dir, &eps)?;
884
885 let exec = executor::Executor::build(graph, weights, ep)?;
886 Ok(Self {
887 inputs,
888 outputs,
889 model_metadata,
890 exec,
891 ep_context_config,
892 })
893 }
894
895 pub fn builder() -> SessionBuilder {
897 SessionBuilder::new()
898 }
899
900 pub fn run(&mut self, inputs: &[(&str, &Tensor)]) -> Result<Vec<Tensor>> {
902 self.exec.run(inputs)
903 }
904
905 pub fn set_trace_context(&mut self, trace: onnx_runtime_tracer::TraceContext) {
910 self.exec.set_trace_context(trace);
911 }
912
913 pub fn run_outputs(&mut self, inputs: &[(&str, &Tensor)]) -> Result<Vec<SessionOutput>> {
915 self.exec.run_outputs(inputs)
916 }
917
918 pub fn decode_memo_counts(&self) -> (u64, u64, u64, u64) {
923 self.exec.decode_memo_counts()
924 }
925
926 pub fn decode_view_plan_counts(&self) -> (u64, u64) {
932 self.exec.decode_view_plan_counts()
933 }
934
935 pub fn run_with_device_bindings(
939 &mut self,
940 inputs: &[(&str, &Tensor)],
941 bindings: &mut [DeviceIoBinding],
942 ) -> Result<Vec<Option<Tensor>>> {
943 self.exec.run_with_device_bindings(inputs, bindings)
944 }
945
946 pub fn allocate_device_binding(
948 &self,
949 input_name: impl Into<String>,
950 output_name: Option<impl Into<String>>,
951 dtype: DataType,
952 physical_shape: Vec<usize>,
953 logical_shape: Vec<usize>,
954 ) -> Result<DeviceIoBinding> {
955 self.exec.allocate_device_binding(
956 input_name.into(),
957 output_name.map(Into::into),
958 dtype,
959 physical_shape,
960 logical_shape,
961 )
962 }
963
964 pub fn allocate_device_output_binding(
967 &self,
968 output_name: impl Into<String>,
969 dtype: DataType,
970 physical_shape: Vec<usize>,
971 logical_shape: Vec<usize>,
972 ) -> Result<DeviceIoBinding> {
973 self.exec.allocate_device_output_binding(
974 output_name.into(),
975 dtype,
976 physical_shape,
977 logical_shape,
978 )
979 }
980
981 pub fn try_capture_with_device_bindings(
986 &mut self,
987 inputs: &[(&str, &Tensor)],
988 bindings: &mut [DeviceIoBinding],
989 ) -> Result<DeviceGraphCaptureResult> {
990 self.exec.try_capture_with_device_bindings(inputs, bindings)
991 }
992
993 pub fn replay_device_graph(&mut self, bindings: &mut [DeviceIoBinding]) -> Result<bool> {
999 self.exec.replay_device_graph(bindings)
1000 }
1001
1002 pub fn reset_device_graph(&mut self) -> Result<bool> {
1005 self.exec.reset_device_graph()
1006 }
1007
1008 pub fn captured_graph_segment_count(&self) -> usize {
1014 self.exec.captured_segment_count()
1015 }
1016
1017 pub fn capture_segmentation(&self) -> &[CaptureDecline] {
1022 self.exec.capture_segmentation()
1023 }
1024
1025 pub fn check_device_capture_error(&self) -> Result<u32> {
1030 self.exec.check_device_capture_error()
1031 }
1032
1033 pub fn device_allocation_counts(&self) -> Option<DeviceAllocationCounts> {
1034 self.exec.device_allocation_counts()
1035 }
1036
1037 pub fn device_id(&self) -> onnx_runtime_ir::DeviceId {
1038 self.exec.device_id()
1039 }
1040
1041 pub fn execution_provider_fallback_report(&self) -> Option<&ExecutionProviderFallbackReport> {
1044 self.exec.execution_provider_fallback_report()
1045 }
1046
1047 pub fn inputs(&self) -> &[IoMeta] {
1049 &self.inputs
1050 }
1051
1052 pub fn outputs(&self) -> &[IoMeta] {
1054 &self.outputs
1055 }
1056
1057 pub fn model_metadata(&self) -> &ModelMetadata {
1059 &self.model_metadata
1060 }
1061
1062 pub fn cache_stats(&self) -> CacheStats {
1064 self.exec.cache_stats()
1065 }
1066
1067 pub fn control_flow_stats(&self) -> ControlFlowStats {
1070 self.exec.control_flow_stats()
1071 }
1072
1073 pub fn warmup(&mut self, shapes: &[WarmupShape]) -> Result<()> {
1078 for ws in shapes {
1079 if !self.inputs.iter().any(|m| m.name == ws.input_name) {
1080 return Err(SessionError::InputNotFound {
1081 name: ws.input_name.clone(),
1082 });
1083 }
1084 }
1085 self.exec.warmup()
1086 }
1087
1088 pub fn ep_context_config(&self) -> &EpContextDumpConfig {
1091 &self.ep_context_config
1092 }
1093
1094 pub fn graph(&self) -> &onnx_runtime_ir::Graph {
1104 self.exec.graph()
1105 }
1106
1107 pub fn export_ep_context(
1134 &self,
1135 orig_path: &Path,
1136 partitions: &[CompiledPartition],
1137 ) -> Result<PathBuf> {
1138 let model = EncoderModel::new(self.exec.graph()).with_weights(self.exec.weights().as_ref());
1139 dump_session_ep_context(&model, orig_path, partitions, &self.ep_context_config)
1140 }
1141}
1142
1143pub fn load(path: impl AsRef<Path>) -> Result<InferenceSession> {
1147 InferenceSession::load(path)
1148}
1149
1150#[cfg(test)]
1151mod device_binding_tests {
1152 use super::*;
1153 #[cfg(feature = "cuda")]
1154 use onnx_runtime_ir::Attribute;
1155 #[cfg(feature = "cuda")]
1156 use onnx_runtime_ir::static_shape;
1157 use onnx_runtime_ir::{Graph, Node, NodeId};
1158
1159 #[test]
1160 fn persistent_binding_aliases_input_output_and_suppresses_materialization() {
1161 let mut graph = Graph::new();
1162 graph.opset_imports.insert("".into(), 13);
1163 let length = graph.intern_symbol("length");
1164 let input = graph.create_named_value("input", DataType::Float32, vec![length.into()]);
1165 graph.add_input(input);
1166 let output = graph.create_named_value("output", DataType::Float32, vec![length.into()]);
1167 graph.insert_node(Node::new(
1168 NodeId(0),
1169 "Relu",
1170 vec![Some(input)],
1171 vec![output],
1172 ));
1173 graph.add_output(output);
1174 let mut session = InferenceSession::from_graph(graph).unwrap();
1175 let mut binding = session
1176 .allocate_device_binding("input", Some("output"), DataType::Float32, vec![4], vec![2])
1177 .unwrap();
1178 let ptr = binding.device_ptr();
1179 let bytes = [-2.0f32, 3.0, -4.0, 5.0]
1180 .into_iter()
1181 .flat_map(f32::to_le_bytes)
1182 .collect::<Vec<_>>();
1183 binding.write_bytes(0, &bytes).unwrap();
1184
1185 let outputs = session
1186 .run_with_device_bindings(&[], std::slice::from_mut(&mut binding))
1187 .unwrap();
1188 assert_eq!(outputs.len(), 1);
1189 assert!(outputs[0].is_none());
1190 assert_eq!(binding.device_ptr(), ptr);
1191 assert_eq!(binding.logical_shape(), &[2]);
1192 let values = binding
1193 .read_bytes()
1194 .unwrap()
1195 .chunks_exact(4)
1196 .map(|bytes| f32::from_le_bytes(bytes.try_into().unwrap()))
1197 .collect::<Vec<_>>();
1198 assert_eq!(values, vec![0.0, 3.0, -4.0, 5.0]);
1199 assert_eq!(
1200 binding.transfer_stats(),
1201 DeviceBindingTransferStats {
1202 host_upload_calls: 1,
1203 host_upload_bytes: 16,
1204 host_download_calls: 1,
1205 host_download_bytes: 16,
1206 }
1207 );
1208 }
1209
1210 #[cfg(feature = "cuda")]
1211 #[test]
1212 fn cuda_graph_replay_uses_persistent_io_without_device_allocations() {
1213 let Ok(mut ep) = onnx_runtime_ep_cuda::CudaExecutionProvider::new(0) else {
1214 eprintln!("skipping session CUDA graph test: CUDA runtime unavailable");
1215 return;
1216 };
1217 onnx_runtime_ep_api::ExecutionProvider::initialize(&mut ep, &Default::default()).unwrap();
1218
1219 let mut graph = Graph::new();
1220 graph.opset_imports.insert("".into(), 13);
1221 let input = graph.create_named_value("input", DataType::Int64, static_shape([1]));
1222 graph.add_input(input);
1223 let output = graph.create_named_value("output", DataType::Float32, static_shape([1]));
1224 let mut cast = Node::new(NodeId(0), "Cast", vec![Some(input)], vec![output]);
1225 cast.attributes
1226 .insert("to".into(), Attribute::Int(DataType::Float32 as i64));
1227 graph.insert_node(cast);
1228 graph.add_output(output);
1229
1230 let mut session = InferenceSession::from_parts(
1231 graph,
1232 std::sync::Arc::new(onnx_runtime_loader::WeightStore::new()),
1233 Path::new("."),
1234 EpContextDumpConfig::default(),
1235 ModelMetadata::default(),
1236 std::sync::Arc::new(ep),
1237 )
1238 .unwrap();
1239 let mut input = session
1240 .allocate_device_binding("input", None::<String>, DataType::Int64, vec![1], vec![1])
1241 .unwrap();
1242 let output = session
1243 .allocate_device_output_binding("output", DataType::Float32, vec![1], vec![1])
1244 .unwrap();
1245 input.write_bytes(0, &7i64.to_le_bytes()).unwrap();
1246 let mut bindings = vec![input, output];
1247 session
1248 .run_with_device_bindings(&[], &mut bindings)
1249 .unwrap();
1250
1251 input_write(&mut bindings[0], 11);
1252 assert!(matches!(
1253 session
1254 .try_capture_with_device_bindings(&[], &mut bindings)
1255 .unwrap(),
1256 DeviceGraphCaptureResult::Captured(_)
1257 ));
1258 assert_eq!(read_bound_f32(&mut bindings[1]), 11.0);
1259
1260 let before = session.device_allocation_counts().unwrap();
1261 input_write(&mut bindings[0], 23);
1262 session.replay_device_graph(&mut bindings).unwrap();
1263 assert_eq!(read_bound_f32(&mut bindings[1]), 23.0);
1264 assert_eq!(session.device_allocation_counts().unwrap(), before);
1265 assert!(session.reset_device_graph().unwrap());
1266
1267 input_write(&mut bindings[0], 31);
1268 assert!(matches!(
1269 session
1270 .try_capture_with_device_bindings(&[], &mut bindings)
1271 .unwrap(),
1272 DeviceGraphCaptureResult::Captured(_)
1273 ));
1274 assert_eq!(read_bound_f32(&mut bindings[1]), 31.0);
1275 input_write(&mut bindings[0], 47);
1276 session.replay_device_graph(&mut bindings).unwrap();
1277 assert_eq!(read_bound_f32(&mut bindings[1]), 47.0);
1278 assert!(session.reset_device_graph().unwrap());
1279 }
1280
1281 #[cfg(feature = "cuda")]
1282 #[test]
1283 fn segmented_cuda_graph_claims_whole_subgraph_around_eager_seam() {
1284 let Ok(mut ep) = onnx_runtime_ep_cuda::CudaExecutionProvider::new(0) else {
1285 eprintln!("skipping segmented session CUDA graph test: CUDA runtime unavailable");
1286 return;
1287 };
1288 onnx_runtime_ep_api::ExecutionProvider::initialize(&mut ep, &Default::default()).unwrap();
1289
1290 let n = 4usize;
1297 let mut graph = Graph::new();
1298 graph.opset_imports.insert("".into(), 13);
1299 let input = graph.create_named_value("input", DataType::Int64, static_shape([n]));
1300 graph.add_input(input);
1301 let as_float = graph.create_named_value("as_float", DataType::Float32, static_shape([n]));
1302 let mut cast_in = Node::new(NodeId(0), "Cast", vec![Some(input)], vec![as_float]);
1303 cast_in
1304 .attributes
1305 .insert("to".into(), Attribute::Int(DataType::Float32 as i64));
1306 graph.insert_node(cast_in);
1307 let clipped = graph.create_named_value("clipped", DataType::Float32, static_shape([n]));
1308 let mut clip = Node::new(NodeId(1), "Clip", vec![Some(as_float)], vec![clipped]);
1309 clip.attributes
1310 .insert("min".into(), Attribute::Float(-1000.0));
1311 clip.attributes
1312 .insert("max".into(), Attribute::Float(1000.0));
1313 graph.insert_node(clip);
1314 let output = graph.create_named_value("output", DataType::Int64, static_shape([n]));
1315 let mut cast_out = Node::new(NodeId(2), "Cast", vec![Some(clipped)], vec![output]);
1316 cast_out
1317 .attributes
1318 .insert("to".into(), Attribute::Int(DataType::Int64 as i64));
1319 graph.insert_node(cast_out);
1320 graph.add_output(output);
1321
1322 let mut session = InferenceSession::from_parts(
1323 graph,
1324 std::sync::Arc::new(onnx_runtime_loader::WeightStore::new()),
1325 Path::new("."),
1326 EpContextDumpConfig::default(),
1327 ModelMetadata::default(),
1328 std::sync::Arc::new(ep),
1329 )
1330 .unwrap();
1331
1332 let input_binding = session
1333 .allocate_device_binding("input", None::<String>, DataType::Int64, vec![n], vec![n])
1334 .unwrap();
1335 let output_binding = session
1336 .allocate_device_output_binding("output", DataType::Int64, vec![n], vec![n])
1337 .unwrap();
1338 let mut bindings = vec![input_binding, output_binding];
1339
1340 let values_a = [-2i64, 3, -4, 5];
1343 write_bound_i64(&mut bindings[0], &values_a);
1344 session
1345 .run_with_device_bindings(&[], &mut bindings)
1346 .unwrap();
1347 let eager_a = read_bound_i64_vec(&mut bindings[1]);
1348 assert_eq!(
1349 eager_a,
1350 values_a.to_vec(),
1351 "Cast∘Clip∘Cast round-trips ints"
1352 );
1353
1354 match session
1357 .try_capture_with_device_bindings(&[], &mut bindings)
1358 .unwrap()
1359 {
1360 DeviceGraphCaptureResult::Captured(outputs) => {
1361 assert!(
1362 outputs.iter().all(Option::is_none),
1363 "device-bound outputs must not materialize to host"
1364 );
1365 }
1366 DeviceGraphCaptureResult::NotCapturable(report) => {
1367 panic!(
1368 "expected the CUDA EP to claim the whole subgraph via segmented capture, \
1369 got a full decline: {report}"
1370 );
1371 }
1372 }
1373 assert_eq!(
1375 session.captured_graph_segment_count(),
1376 2,
1377 "Clip should split the plan into two captured Cast segments"
1378 );
1379 let seams = session.capture_segmentation();
1380 assert_eq!(seams.len(), 1, "exactly one eager seam node (Clip)");
1381 assert_eq!(seams[0].op_type, "Clip");
1382 assert_eq!(read_bound_i64_vec(&mut bindings[1]), eager_a);
1384
1385 let values_b = [7i64, -1, 0, -8];
1388 write_bound_i64(&mut bindings[0], &values_b);
1389 session.replay_device_graph(&mut bindings).unwrap();
1390 let replay_b = read_bound_i64_vec(&mut bindings[1]);
1391
1392 assert!(session.reset_device_graph().unwrap());
1394 write_bound_i64(&mut bindings[0], &values_b);
1395 session
1396 .run_with_device_bindings(&[], &mut bindings)
1397 .unwrap();
1398 let eager_b = read_bound_i64_vec(&mut bindings[1]);
1399 assert_eq!(
1400 replay_b, eager_b,
1401 "segmented replay must be bit-identical to eager execution"
1402 );
1403 assert_eq!(replay_b, values_b.to_vec());
1404 }
1405
1406 #[cfg(feature = "cuda")]
1407 fn write_bound_i64(binding: &mut DeviceIoBinding, values: &[i64]) {
1408 let bytes = values
1409 .iter()
1410 .flat_map(|value| value.to_le_bytes())
1411 .collect::<Vec<_>>();
1412 binding.write_bytes(0, &bytes).unwrap();
1413 }
1414
1415 #[cfg(feature = "cuda")]
1416 fn read_bound_i64_vec(binding: &mut DeviceIoBinding) -> Vec<i64> {
1417 binding
1418 .read_bytes()
1419 .unwrap()
1420 .chunks_exact(8)
1421 .map(|bytes| i64::from_le_bytes(bytes.try_into().unwrap()))
1422 .collect()
1423 }
1424
1425 #[cfg(feature = "cuda")]
1426 fn input_write(binding: &mut DeviceIoBinding, value: i64) {
1427 binding.write_bytes(0, &value.to_le_bytes()).unwrap();
1428 }
1429
1430 #[cfg(feature = "cuda")]
1431 fn read_bound_f32(binding: &mut DeviceIoBinding) -> f32 {
1432 let bytes = binding.read_bytes().unwrap();
1433 f32::from_le_bytes(bytes.try_into().unwrap())
1434 }
1435}
1436
1437#[cfg(test)]
1438mod option_tests {
1439 use super::*;
1440
1441 fn opts(pairs: &[(&str, &str)]) -> HashMap<String, String> {
1442 pairs
1443 .iter()
1444 .map(|(k, v)| (k.to_string(), v.to_string()))
1445 .collect()
1446 }
1447
1448 fn level_of(pairs: &[(&str, &str)]) -> Result<OptimizationLevel> {
1449 SessionBuilder::parse_options(&opts(pairs)).map(|(level, _)| level)
1450 }
1451
1452 fn ctx_of(pairs: &[(&str, &str)]) -> Result<EpContextDumpConfig> {
1453 SessionBuilder::parse_options(&opts(pairs)).map(|(_, ctx)| ctx)
1454 }
1455
1456 #[test]
1457 fn optimization_defaults_to_none_when_unset() {
1458 assert_eq!(level_of(&[]).unwrap(), OptimizationLevel::None);
1459 }
1460
1461 #[test]
1462 fn explicit_execution_provider_is_retained_by_builder() {
1463 let builder =
1464 SessionBuilder::new().execution_provider(executor::auto_detect_cpu_ep().unwrap());
1465 assert!(builder.execution_provider.is_some());
1466 }
1467
1468 #[test]
1469 fn optimization_parses_known_values() {
1470 for (v, want) in [
1471 ("none", OptimizationLevel::None),
1472 ("off", OptimizationLevel::None),
1473 ("BASIC", OptimizationLevel::Basic),
1474 ("All", OptimizationLevel::All),
1475 ] {
1476 assert_eq!(
1477 level_of(&[("optimization", v)]).unwrap(),
1478 want,
1479 "value {v:?}"
1480 );
1481 }
1482 }
1483
1484 #[test]
1485 fn unknown_option_key_is_rejected() {
1486 let err = level_of(&[("optimisation", "all")]).unwrap_err();
1487 assert!(matches!(err, SessionError::UnknownOption { key } if key == "optimisation"));
1488 }
1489
1490 #[test]
1491 fn invalid_optimization_value_is_rejected() {
1492 let err = level_of(&[("optimization", "aggressive")]).unwrap_err();
1493 assert!(matches!(
1494 err,
1495 SessionError::InvalidOption { key, value, .. } if key == "optimization" && value == "aggressive"
1496 ));
1497 }
1498
1499 #[test]
1500 fn none_level_selects_no_passes() {
1501 assert!(OptimizationLevel::None.passes().is_empty());
1502 assert_eq!(OptimizationLevel::Basic.passes().len(), 2);
1503 assert_eq!(OptimizationLevel::All.passes().len(), 3);
1504 }
1505
1506 #[test]
1509 fn ep_context_defaults_to_disabled() {
1510 let ctx = ctx_of(&[]).unwrap();
1511 assert_eq!(ctx, EpContextDumpConfig::default());
1512 assert!(!ctx.enable);
1513 assert_eq!(ctx.file_path, None);
1514 assert_eq!(ctx.embed_mode, 1);
1515 }
1516
1517 #[test]
1518 fn ep_context_enable_parses_bool_forms() {
1519 for (v, want) in [
1520 ("1", true),
1521 ("0", false),
1522 ("true", true),
1523 ("TRUE", true),
1524 ("false", false),
1525 ("False", false),
1526 ] {
1527 let ctx = ctx_of(&[("ep.context_enable", v)]).unwrap();
1528 assert_eq!(ctx.enable, want, "value {v:?}");
1529 }
1530 }
1531
1532 #[test]
1533 fn ep_context_enable_rejects_garbage() {
1534 let err = ctx_of(&[("ep.context_enable", "yes")]).unwrap_err();
1535 assert!(matches!(
1536 err,
1537 SessionError::InvalidOption { key, value, .. }
1538 if key == "ep.context_enable" && value == "yes"
1539 ));
1540 }
1541
1542 #[test]
1543 fn ep_context_file_path_parses_and_empty_clears() {
1544 let ctx = ctx_of(&[("ep.context_file_path", "/out/net_ctx.onnx")]).unwrap();
1545 assert_eq!(ctx.file_path, Some(PathBuf::from("/out/net_ctx.onnx")));
1546
1547 let ctx = ctx_of(&[("ep.context_file_path", "")]).unwrap();
1549 assert_eq!(ctx.file_path, None);
1550 }
1551
1552 #[test]
1553 fn ep_context_embed_mode_parses_and_rejects() {
1554 assert_eq!(
1555 ctx_of(&[("ep.context_embed_mode", "0")])
1556 .unwrap()
1557 .embed_mode,
1558 0
1559 );
1560 assert_eq!(
1561 ctx_of(&[("ep.context_embed_mode", "1")])
1562 .unwrap()
1563 .embed_mode,
1564 1
1565 );
1566
1567 let err = ctx_of(&[("ep.context_embed_mode", "2")]).unwrap_err();
1568 assert!(matches!(
1569 err,
1570 SessionError::InvalidOption { key, value, expected }
1571 if key == "ep.context_embed_mode" && value == "2" && expected == "0, 1"
1572 ));
1573 }
1574
1575 #[test]
1576 fn ep_context_options_combine_with_optimization() {
1577 let (level, ctx) = SessionBuilder::parse_options(&opts(&[
1578 ("optimization", "all"),
1579 ("ep.context_enable", "1"),
1580 ("ep.context_file_path", "/tmp/out_ctx.onnx"),
1581 ("ep.context_embed_mode", "0"),
1582 ]))
1583 .unwrap();
1584 assert_eq!(level, OptimizationLevel::All);
1585 assert!(ctx.enable);
1586 assert_eq!(ctx.file_path, Some(PathBuf::from("/tmp/out_ctx.onnx")));
1587 assert_eq!(ctx.embed_mode, 0);
1588 }
1589}