1use crate::engine::ModelFormat;
3use crate::session::ActiveSession;
4use crate::{
5 Backend, BackendSelection, CompileOptions, OnnxOptions, Result, Tensor, TensorSpec, TensorView,
6 TensorViewMut,
7};
8use crate::{engine, session};
9use anyhow::{Context, bail, ensure};
10use std::{
11 fs,
12 marker::PhantomData,
13 path::{Path, PathBuf},
14 rc::Rc,
15};
16
17#[derive(Clone, Debug)]
19pub struct ModelInfo {
20 pub inputs: Vec<TensorSpec>,
22 pub outputs: Vec<TensorSpec>,
24}
25
26#[derive(Clone, Debug)]
28pub struct FallbackEvent {
29 pub backend: String,
31 pub error: String,
33}
34
35fn failure_report(events: &[FallbackEvent]) -> String {
36 events
37 .iter()
38 .map(|event| format!(" {}: {}", event.backend, event.error))
39 .collect::<Vec<_>>()
40 .join("\n")
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum CompiledFormat {
46 OptimizedOnnx,
48 EpContext,
50}
51
52#[derive(Clone, Debug)]
54pub struct Compilation {
55 pub backend: String,
57 pub format: CompiledFormat,
59}
60
61pub struct OnnxRuntime {
64 path: PathBuf,
65 options: OnnxOptions,
66 info: ModelInfo,
67 active: Option<ActiveSession>,
68 fallback_events: Vec<FallbackEvent>,
69 is_ep_context: bool,
70 _no_send_sync: PhantomData<Rc<()>>,
71}
72
73impl OnnxRuntime {
74 pub fn load(path: impl AsRef<Path>, options: OnnxOptions) -> Result<Self> {
77 Self::load_internal(path.as_ref(), options, None)
78 }
79
80 pub fn compile(
92 source: impl AsRef<Path>,
93 destination: impl AsRef<Path>,
94 options: CompileOptions,
95 inputs: &[TensorView<'_>],
96 ) -> Result<Compilation> {
97 let destination = destination.as_ref();
98 ensure!(!destination.exists(), "Compiled output already exists");
99 let options = options.into_runtime_options()?;
100 options.validate()?;
101 session::initialize(&options)?;
102 let source = source.as_ref().canonicalize().context("ONNX source path")?;
103 ensure!(
104 matches!(engine::inspect(&source, &options)?.0, ModelFormat::Onnx),
105 "Compile requires an ONNX graph, not an existing EPContext"
106 );
107 let backend = options.backend.candidates()[0].name().to_owned();
108 let (bytes, format) = Self::compile_and_validate(&source, options, inputs)
109 .with_context(|| format!("Compile for {backend}"))?;
110 use std::io::Write;
111 let parent = destination
112 .parent()
113 .filter(|p| !p.as_os_str().is_empty())
114 .unwrap_or_else(|| Path::new("."));
115 let mut file = tempfile::Builder::new()
116 .prefix(".native-onnx-")
117 .tempfile_in(parent)?;
118 file.write_all(&bytes)?;
119 file.as_file().sync_all()?;
120 file.persist_noclobber(destination).map_err(|error| {
121 anyhow::anyhow!(
123 "Publish compiled model {}: {}",
124 destination.display(),
125 error.error
126 )
127 })?;
128 Ok(Compilation { backend, format })
129 }
130
131 pub fn backend(&self) -> Option<&str> {
133 self.active
134 .as_ref()
135 .map(|a| self.options.backend.candidates()[a.backend_index].name())
136 }
137 pub fn fallback_events(&self) -> &[FallbackEvent] {
139 &self.fallback_events
140 }
141 pub fn is_prepared(&self) -> bool {
143 self.active.as_ref().is_some_and(|a| a.buffers.is_some())
144 }
145 pub fn end_profiling(&mut self) -> Result<String> {
148 Ok(self
149 .active
150 .as_mut()
151 .context("No active backend")?
152 .session
153 .end_profiling()?)
154 }
155}
156
157impl OnnxRuntime {
158 pub fn info(&self) -> &ModelInfo {
160 &self.info
161 }
162 pub fn inference(&mut self, inputs: &[TensorView<'_>]) -> Result<Vec<Tensor>> {
164 self.validate_inputs(inputs)?;
165 loop {
166 let active = self.active.as_mut().context("No active backend")?;
167 let result = active.inference(&self.info, inputs);
168 match result {
169 Ok(values) => {
170 return Ok(values);
171 }
172 Err(e) => self.fallback(e)?,
173 }
174 }
175 }
176 pub fn inference_into(
178 &mut self,
179 inputs: &[TensorView<'_>],
180 outputs: &mut [TensorViewMut<'_>],
181 ) -> Result<()> {
182 self.validate_inputs(inputs)?;
183 if !self.is_prepared() {
184 return crate::tensor::copy_outputs(&self.inference(inputs)?, outputs);
185 }
186 ensure!(
187 outputs.len() == self.info.outputs.len(),
188 "Output count mismatch"
189 );
190 for (i, output) in outputs.iter().enumerate() {
191 ensure!(
192 !outputs[..i].iter().any(|x| x.name == output.name),
193 "Duplicate output {}",
194 output.name
195 );
196 let spec = self
197 .info
198 .outputs
199 .iter()
200 .find(|s| s.name == output.name)
201 .context("Unknown output")?;
202 ensure!(
203 output.data.dtype() == spec.dtype
204 && spec.fixed_shape().as_deref() == Some(output.shape)
205 && output.data.len() == crate::element_count(output.shape)?,
206 "Output shape/type/length mismatch"
207 );
208 }
209 loop {
210 if !self.is_prepared() {
211 return crate::tensor::copy_outputs(&self.inference(inputs)?, outputs);
212 }
213 for input in inputs {
214 self.input_mut(input.name)?.data.copy_from(input.data)?;
215 }
216 let active = self.active.as_mut().context("No active backend")?;
217 match active
218 .buffers
219 .as_mut()
220 .expect("prepared backend")
221 .run(&mut active.session)
222 {
223 Ok(()) => {
224 break;
225 }
226 Err(error) => self.fallback(error)?,
227 }
228 }
229 for output in outputs {
230 output.data.copy_from(self.output(output.name)?.data)?;
231 }
232 Ok(())
233 }
234}
235
236impl OnnxRuntime {
237 pub fn input_mut(&mut self, name: &str) -> Result<TensorViewMut<'_>> {
240 let buffers = self
241 .active
242 .as_mut()
243 .and_then(|a| a.buffers.as_mut())
244 .context("Prepared buffers unavailable; use inference for dynamic I/O")?;
245 buffers.outputs_valid = false;
246 buffers
247 .inputs
248 .iter_mut()
249 .find(|s| s.name == name)
250 .context("Unknown input")?
251 .view_mut()
252 }
253 pub fn output(&self, name: &str) -> Result<TensorView<'_>> {
255 let buffers = self
256 .active
257 .as_ref()
258 .and_then(|a| a.buffers.as_ref())
259 .context("Prepared buffers unavailable")?;
260 ensure!(
261 buffers.outputs_valid,
262 "No successful output for current input"
263 );
264 buffers
265 .outputs
266 .iter()
267 .find(|s| s.name == name)
268 .context("Unknown output")?
269 .view()
270 }
271 pub fn run(&mut self) -> Result<()> {
273 let active = self.active.as_mut().context("No active backend")?;
274 let buffers = active
275 .buffers
276 .as_mut()
277 .context("Prepared buffers unavailable")?;
278 match buffers.run(&mut active.session) {
279 Ok(()) => Ok(()),
280 Err(error) => {
281 let inputs = buffers
283 .inputs
284 .iter()
285 .map(|s| Ok(s.view()?.to_owned()))
286 .collect::<Result<Vec<_>>>()?;
287 self.fallback(error)?;
288 let views = inputs.iter().map(Tensor::view).collect::<Vec<_>>();
289 self.inference(&views)?;
290 ensure!(
291 self.is_prepared(),
292 "Fallback ran successfully but has no prepared output; use inference for custom providers"
293 );
294 Ok(())
295 }
296 }
297 }
298}
299
300impl OnnxRuntime {
301 fn compile_and_validate(
302 source: &Path,
303 mut options: OnnxOptions,
304 inputs: &[TensorView<'_>],
305 ) -> Result<(Vec<u8>, CompiledFormat)> {
306 let directory = engine::BuildDirectory::new()?;
307 let bytes = if matches!(
308 options.backend,
309 BackendSelection::Require(Backend::TensorRt)
310 ) {
311 options.profiling = Some(directory.path().join("compile-profile"));
312 let mut model = Self::load_internal(source, options.clone(), Some(directory.path()))?;
313 model.inference(inputs)?;
314 let profile = model.end_profiling()?;
315 engine::export_from_profile(directory.path(), &model.info, Path::new(&profile))?
316 } else {
317 session::compile(source, &options)?
318 };
319 let staged = directory.path().join("compiled.onnx");
320 fs::write(&staged, &bytes)?;
321 let format = match engine::inspect(&staged, &options)?.0 {
322 ModelFormat::Onnx => CompiledFormat::OptimizedOnnx,
323 ModelFormat::EpContext => CompiledFormat::EpContext,
324 };
325 options.profiling = None;
327 let mut model = Self::load(&staged, options)?;
328 model.inference(inputs)?;
329 Ok((bytes, format))
330 }
331
332 fn load_internal(
333 path: &Path,
334 options: OnnxOptions,
335 build_directory: Option<&Path>,
336 ) -> Result<Self> {
337 options.validate()?;
338 session::initialize(&options)?;
339 let path = path.canonicalize().context("Model path")?;
340 let (format, info) = engine::inspect(&path, &options)?;
341 let mut model = Self {
342 path,
343 options,
344 info,
345 active: None,
346 fallback_events: Vec::new(),
347 is_ep_context: matches!(format, ModelFormat::EpContext),
348 _no_send_sync: PhantomData,
349 };
350 if let ModelFormat::EpContext = format {
351 ensure!(
352 build_directory.is_none(),
353 "Compile requires the original ONNX graph"
354 );
355 let mut failures = Vec::new();
356 for index in 0..model.options.backend.candidates().len() {
357 match session::load(&model.path, &model.options, index, true, &model.info, None) {
358 Ok(active) => {
359 model.active = Some(active);
360 break;
361 }
362 Err(error) => failures.push(FallbackEvent {
363 backend: model.options.backend.candidates()[index].name().into(),
364 error: format!("{error:#}"),
365 }),
366 }
367 }
368 ensure!(
369 model.active.is_some(),
370 "No configured backend could load the EPContext:\n{}",
371 failure_report(&failures)
372 );
373 model.fallback_events = failures;
374 } else {
375 model.activate(0, build_directory)?;
376 }
377 model.info = model
378 .active
379 .as_ref()
380 .context("No active backend")?
381 .info
382 .clone();
383 Ok(model)
384 }
385
386 fn activate(&mut self, start: usize, build_directory: Option<&Path>) -> Result<()> {
387 self.active = None; for index in start..self.options.backend.candidates().len() {
389 match session::load(
390 &self.path,
391 &self.options,
392 index,
393 false,
394 &self.info,
395 build_directory,
396 ) {
397 Ok(active) => {
398 self.info = active.info.clone();
399 self.active = Some(active);
400 return Ok(());
401 }
402 Err(error) => self.fallback_events.push(FallbackEvent {
403 backend: self.options.backend.candidates()[index].name().into(),
404 error: format!("{error:#}"),
405 }),
406 }
407 }
408 bail!(
409 "All configured ONNX backends failed:\n{}",
410 failure_report(&self.fallback_events)
411 )
412 }
413
414 fn validate_inputs(&self, inputs: &[TensorView<'_>]) -> Result<()> {
415 ensure!(
416 inputs.len() == self.info.inputs.len(),
417 "Input count mismatch"
418 );
419 for (i, input) in inputs.iter().enumerate() {
420 ensure!(
421 !inputs[..i].iter().any(|x| x.name == input.name),
422 "Duplicate input {}",
423 input.name
424 );
425 self.info
426 .inputs
427 .iter()
428 .find(|s| s.name == input.name)
429 .with_context(|| format!("Unknown input {}", input.name))?
430 .validate(input)?;
431 }
432 Ok(())
433 }
434
435 fn fallback(&mut self, error: anyhow::Error) -> Result<()> {
436 let index = self
437 .active
438 .as_ref()
439 .context("No active backend")?
440 .backend_index;
441 if self.is_ep_context || matches!(self.options.backend, BackendSelection::Require(_)) {
442 return Err(error);
443 }
444 self.fallback_events.push(FallbackEvent {
445 backend: self.options.backend.candidates()[index].name().into(),
446 error: format!("{error:#}"),
447 });
448 self.activate(index + 1, None)
449 }
450}