1use std::sync::Arc;
4
5use ort::{
6 ep::CPU,
7 memory::MemoryInfo,
8 session::{builder::GraphOptimizationLevel, Session},
9 value::{DynValue, Tensor, TensorElementType, TensorRef, TensorValueType, ValueType},
10};
11use spatialrust_tensor::{DataType, Device, HostTensorStorage, TensorBuffer, TensorDescriptor};
12
13use crate::{
14 AiError, AiResult, CopyPolicy, Dimension, GraphOptimization, InferenceBackend,
15 IoBinding as SpatialIoBinding, ModelInfo, ModelSession, ModelSource, NamedTensors,
16 OutputBinding, PreallocatedStorage, RunOptions, SessionOptions, TensorSpec,
17};
18
19const BACKEND_NAME: &str = "onnxruntime-cpu";
20
21#[derive(Clone, Copy, Debug, Default)]
23pub struct OnnxRuntimeBackend;
24
25impl InferenceBackend for OnnxRuntimeBackend {
26 fn name(&self) -> &str {
27 BACKEND_NAME
28 }
29
30 fn create_session(
31 &self,
32 source: &ModelSource,
33 options: &SessionOptions,
34 ) -> AiResult<Box<dyn ModelSession>> {
35 options.validate()?;
36 let mut builder = Session::builder().map_err(ort_error)?;
37 builder = builder.with_execution_providers([CPU::default().build()]).map_err(ort_error)?;
38 if let Some(threads) = options.intra_threads {
39 builder = builder.with_intra_threads(threads).map_err(ort_error)?;
40 }
41 if let Some(threads) = options.inter_threads {
42 builder = builder.with_inter_threads(threads).map_err(ort_error)?;
43 }
44 builder = builder
45 .with_optimization_level(match options.graph_optimization {
46 GraphOptimization::Disabled => GraphOptimizationLevel::Disable,
47 GraphOptimization::Basic => GraphOptimizationLevel::Level1,
48 GraphOptimization::Extended => GraphOptimizationLevel::Level2,
49 GraphOptimization::All => GraphOptimizationLevel::Level3,
50 })
51 .map_err(ort_error)?;
52 builder = builder.with_deterministic_compute(options.deterministic).map_err(ort_error)?;
53 let session = match source {
54 ModelSource::Path(path) => builder.commit_from_file(path).map_err(ort_error)?,
55 ModelSource::Bytes(bytes) => builder.commit_from_memory(bytes).map_err(ort_error)?,
56 ModelSource::Mock(_) => {
57 return Err(AiError::Unsupported {
58 backend: BACKEND_NAME.into(),
59 operation: "ModelSource::Mock (use MockInferenceBackend)".into(),
60 });
61 }
62 };
63 let info = model_info(&session)?;
64 Ok(Box::new(OnnxRuntimeSession { session, info }))
65 }
66}
67
68#[derive(Debug)]
70pub struct OnnxRuntimeSession {
71 session: Session,
72 info: ModelInfo,
73}
74
75impl ModelSession for OnnxRuntimeSession {
76 fn backend_name(&self) -> &str {
77 BACKEND_NAME
78 }
79
80 fn model_info(&self) -> &ModelInfo {
81 &self.info
82 }
83
84 fn run_with_options(
85 &mut self,
86 inputs: NamedTensors,
87 options: RunOptions,
88 ) -> AiResult<NamedTensors> {
89 self.info.validate_inputs(&inputs)?;
90 if options.input_copy == CopyPolicy::Forbid {
91 return Err(AiError::CopyRequired {
92 direction: "input host",
93 name: self.info.inputs.first().map_or_else(String::new, |spec| spec.name.clone()),
94 });
95 }
96 if options.output_copy == CopyPolicy::Forbid {
97 return Err(AiError::CopyRequired {
98 direction: "output host",
99 name: self.info.outputs.first().map_or_else(String::new, |spec| spec.name.clone()),
100 });
101 }
102 let values = inputs
103 .iter()
104 .map(|(name, tensor)| Ok((name.to_owned(), to_ort_tensor(name, tensor)?)))
105 .collect::<AiResult<Vec<_>>>()?;
106 let outputs = self.session.run(values).map_err(ort_error)?;
107 let mut named = NamedTensors::new();
108 for spec in &self.info.outputs {
109 let value = outputs.get(&spec.name).ok_or_else(|| AiError::Backend {
110 backend: BACKEND_NAME.into(),
111 message: format!("runtime omitted output `{}`", spec.name),
112 })?;
113 named.insert(spec.name.clone(), copy_ort_output(spec, value)?)?;
114 }
115 Ok(named)
116 }
117
118 fn run_with_binding(&mut self, binding: &mut SpatialIoBinding) -> AiResult<()> {
119 binding.clear_results();
120 self.info.validate_inputs(binding.inputs())?;
121 let mut ort_binding = self.session.create_binding().map_err(ort_error)?;
122 for (name, tensor) in binding.inputs().iter() {
123 bind_zero_copy_input(&mut ort_binding, name, tensor)?;
124 }
125
126 let requested = binding
127 .outputs()
128 .iter()
129 .map(|output| match output {
130 OutputBinding::Allocate { name, .. } => name.clone(),
131 OutputBinding::PreallocatedCpu(output) => output.name().to_owned(),
132 })
133 .collect::<Vec<_>>();
134 for output in binding.outputs_mut() {
135 match output {
136 OutputBinding::Allocate { name, device } => {
137 output_spec(&self.info, name)?;
138 if *device != Device::CPU {
139 return Err(AiError::Unsupported {
140 backend: BACKEND_NAME.into(),
141 operation: format!("bound output device {device:?}"),
142 });
143 }
144 ort_binding
145 .bind_output_to_device(name.clone(), &MemoryInfo::default())
146 .map_err(ort_error)?;
147 }
148 OutputBinding::PreallocatedCpu(output) => {
149 let spec = output_spec(&self.info, output.name())?;
150 spec.validate(output.descriptor())?;
151 if !output.descriptor().is_c_contiguous()
152 || output.descriptor().byte_offset() != 0
153 {
154 return Err(AiError::Unsupported {
155 backend: BACKEND_NAME.into(),
156 operation: format!(
157 "strided preallocated output `{}`; allocate a compact buffer",
158 output.name()
159 ),
160 });
161 }
162 let name = output.name().to_owned();
163 let shape = output.descriptor().shape().to_vec();
164 bind_preallocated_output(
165 &mut ort_binding,
166 &name,
167 shape,
168 output.take_storage()?,
169 spec.dtype,
170 )?;
171 }
172 }
173 }
174
175 let mut outputs = self.session.run_binding(&ort_binding).map_err(ort_error)?;
176 let mut results = NamedTensors::new();
177 for name in requested {
178 let spec = output_spec(&self.info, &name)?;
179 let value = outputs.remove(&name).ok_or_else(|| AiError::Backend {
180 backend: BACKEND_NAME.into(),
181 message: format!("runtime omitted bound output `{name}`"),
182 })?;
183 results.insert(name, retain_ort_output(spec, value)?)?;
184 }
185 drop(outputs);
186 binding.set_results(results);
187 Ok(())
188 }
189}
190
191fn output_spec<'a>(info: &'a ModelInfo, name: &str) -> AiResult<&'a TensorSpec> {
192 info.outputs
193 .iter()
194 .find(|spec| spec.name == name)
195 .ok_or_else(|| AiError::UnexpectedTensor(name.to_owned()))
196}
197
198fn bind_zero_copy_input(
199 binding: &mut ort::session::IoBinding,
200 name: &str,
201 tensor: &TensorBuffer,
202) -> AiResult<()> {
203 let descriptor = tensor.descriptor();
204 if !descriptor.is_c_contiguous() || descriptor.byte_offset() != 0 {
205 return Err(AiError::Unsupported {
206 backend: BACKEND_NAME.into(),
207 operation: format!("strided bound input `{name}`; explicitly pack it first"),
208 });
209 }
210 if let Some(storage) = tensor.host_storage() {
211 if let Some(storage) = storage.as_any().downcast_ref::<OrtHostStorage>() {
212 return storage.bind_input(binding, name);
213 }
214 }
215 let expected = descriptor
216 .element_count()
217 .map_err(|error| AiError::InvalidConfiguration(error.to_string()))?;
218 let shape = descriptor.shape().to_vec();
219 macro_rules! bind_arc {
220 ($getter:ident, $type:ty) => {{
221 let values = tensor.$getter().ok_or_else(|| AiError::CopyRequired {
222 direction: "input alignment",
223 name: name.to_owned(),
224 })?;
225 if values.len() != expected {
226 return Err(AiError::InvalidConfiguration(format!(
227 "bound input `{name}` allocation has {} elements, expected {expected}",
228 values.len()
229 )));
230 }
231 let value = TensorRef::<$type>::from_array_view((shape, values)).map_err(ort_error)?;
232 binding.bind_input(name, &value).map_err(ort_error)
233 }};
234 }
235 match descriptor.dtype() {
236 DataType::U8 => bind_arc!(shared_bytes, u8),
237 DataType::U16 => bind_arc!(shared_u16, u16),
238 DataType::U32 => bind_arc!(shared_u32, u32),
239 DataType::I16 => bind_arc!(shared_i16, i16),
240 DataType::I32 => bind_arc!(shared_i32, i32),
241 DataType::I64 => bind_arc!(shared_i64, i64),
242 DataType::F32 => bind_arc!(shared_f32, f32),
243 DataType::F64 => bind_arc!(shared_f64, f64),
244 dtype => Err(AiError::Unsupported {
245 backend: BACKEND_NAME.into(),
246 operation: format!("zero-copy bound input dtype {dtype:?}"),
247 }),
248 }
249}
250
251fn bind_preallocated_output(
252 binding: &mut ort::session::IoBinding,
253 name: &str,
254 shape: Vec<usize>,
255 storage: PreallocatedStorage,
256 dtype: DataType,
257) -> AiResult<()> {
258 match (storage, dtype) {
259 (PreallocatedStorage::Bytes(values), DataType::U8) => binding
260 .bind_output(name, Tensor::<u8>::from_array((shape, values)).map_err(ort_error)?)
261 .map_err(ort_error),
262 (PreallocatedStorage::U16(values), DataType::U16) => binding
263 .bind_output(name, Tensor::<u16>::from_array((shape, values)).map_err(ort_error)?)
264 .map_err(ort_error),
265 (PreallocatedStorage::F32(values), DataType::F32) => binding
266 .bind_output(name, Tensor::<f32>::from_array((shape, values)).map_err(ort_error)?)
267 .map_err(ort_error),
268 _ => Err(AiError::CopyRequired { direction: "output alignment", name: name.to_owned() }),
269 }
270}
271
272#[derive(Debug)]
273enum OrtHostStorage {
274 U8(Tensor<u8>),
275 U16(Tensor<u16>),
276 U32(Tensor<u32>),
277 I8(Tensor<i8>),
278 I16(Tensor<i16>),
279 I32(Tensor<i32>),
280 I64(Tensor<i64>),
281 F32(Tensor<f32>),
282 F64(Tensor<f64>),
283}
284
285impl OrtHostStorage {
286 fn bind_input(&self, binding: &mut ort::session::IoBinding, name: &str) -> AiResult<()> {
287 macro_rules! bind {
288 ($value:expr) => {
289 binding.bind_input(name, $value).map_err(ort_error)
290 };
291 }
292 match self {
293 Self::U8(value) => bind!(value),
294 Self::U16(value) => bind!(value),
295 Self::U32(value) => bind!(value),
296 Self::I8(value) => bind!(value),
297 Self::I16(value) => bind!(value),
298 Self::I32(value) => bind!(value),
299 Self::I64(value) => bind!(value),
300 Self::F32(value) => bind!(value),
301 Self::F64(value) => bind!(value),
302 }
303 }
304}
305
306impl HostTensorStorage for OrtHostStorage {
307 fn dtype(&self) -> DataType {
308 match self {
309 Self::U8(_) => DataType::U8,
310 Self::U16(_) => DataType::U16,
311 Self::U32(_) => DataType::U32,
312 Self::I8(_) => DataType::I8,
313 Self::I16(_) => DataType::I16,
314 Self::I32(_) => DataType::I32,
315 Self::I64(_) => DataType::I64,
316 Self::F32(_) => DataType::F32,
317 Self::F64(_) => DataType::F64,
318 }
319 }
320
321 fn allocation_bytes(&self) -> &[u8] {
322 macro_rules! bytes {
323 ($value:expr) => {
324 bytemuck::cast_slice($value.extract_tensor().1)
325 };
326 }
327 match self {
328 Self::U8(value) => value.extract_tensor().1,
329 Self::U16(value) => bytes!(value),
330 Self::U32(value) => bytes!(value),
331 Self::I8(value) => bytes!(value),
332 Self::I16(value) => bytes!(value),
333 Self::I32(value) => bytes!(value),
334 Self::I64(value) => bytes!(value),
335 Self::F32(value) => bytes!(value),
336 Self::F64(value) => bytes!(value),
337 }
338 }
339
340 fn as_any(&self) -> &dyn std::any::Any {
341 self
342 }
343}
344
345fn retain_ort_output(spec: &TensorSpec, value: DynValue) -> AiResult<TensorBuffer> {
346 macro_rules! retain {
347 ($type:ty, $variant:ident) => {{
348 let tensor = value.downcast::<TensorValueType<$type>>().map_err(ort_error)?;
349 let shape = tensor
350 .extract_tensor()
351 .0
352 .iter()
353 .map(|&dimension| usize::try_from(dimension).expect("runtime shape is concrete"))
354 .collect::<Vec<_>>();
355 let storage: Arc<dyn HostTensorStorage> = Arc::new(OrtHostStorage::$variant(tensor));
356 (shape, storage)
357 }};
358 }
359 let (shape, storage) = match spec.dtype {
360 DataType::U8 => retain!(u8, U8),
361 DataType::U16 => retain!(u16, U16),
362 DataType::U32 => retain!(u32, U32),
363 DataType::I8 => retain!(i8, I8),
364 DataType::I16 => retain!(i16, I16),
365 DataType::I32 => retain!(i32, I32),
366 DataType::I64 => retain!(i64, I64),
367 DataType::F32 => retain!(f32, F32),
368 DataType::F64 => retain!(f64, F64),
369 dtype => {
370 return Err(AiError::Unsupported {
371 backend: BACKEND_NAME.into(),
372 operation: format!("retaining bound output dtype {dtype:?}"),
373 })
374 }
375 };
376 TensorBuffer::try_from_host_storage(
377 storage,
378 TensorDescriptor::contiguous(spec.dtype, shape, Device::CPU),
379 )
380 .map_err(|error| AiError::Backend { backend: BACKEND_NAME.into(), message: error.to_string() })
381}
382
383fn model_info(session: &Session) -> AiResult<ModelInfo> {
384 let inputs = session.inputs().iter().map(outlet_spec).collect::<AiResult<Vec<_>>>()?;
385 let outputs = session.outputs().iter().map(outlet_spec).collect::<AiResult<Vec<_>>>()?;
386 let info = ModelInfo { name: None, inputs, outputs };
387 info.validate()?;
388 Ok(info)
389}
390
391fn outlet_spec(outlet: &ort::value::Outlet) -> AiResult<TensorSpec> {
392 let ValueType::Tensor { ty, shape, dimension_symbols } = outlet.dtype() else {
393 return Err(AiError::Unsupported {
394 backend: BACKEND_NAME.into(),
395 operation: format!("non-tensor ONNX value `{}`", outlet.name()),
396 });
397 };
398 let dtype = decode_ort_dtype(*ty).ok_or_else(|| AiError::Unsupported {
399 backend: BACKEND_NAME.into(),
400 operation: format!("ONNX dtype {ty} for `{}`", outlet.name()),
401 })?;
402 let dimensions = shape
403 .iter()
404 .enumerate()
405 .map(|(index, &dimension)| {
406 if dimension >= 0 {
407 Dimension::Fixed(dimension as usize)
408 } else {
409 let symbol = &dimension_symbols[index];
410 if symbol.is_empty() {
411 Dimension::Dynamic
412 } else {
413 Dimension::Symbol(symbol.clone())
414 }
415 }
416 })
417 .collect();
418 Ok(TensorSpec::new(outlet.name(), dtype, dimensions))
419}
420
421fn decode_ort_dtype(dtype: TensorElementType) -> Option<DataType> {
422 Some(match dtype {
423 TensorElementType::Float32 => DataType::F32,
424 TensorElementType::Float64 => DataType::F64,
425 TensorElementType::Uint8 => DataType::U8,
426 TensorElementType::Uint16 => DataType::U16,
427 TensorElementType::Uint32 => DataType::U32,
428 TensorElementType::Int8 => DataType::I8,
429 TensorElementType::Int16 => DataType::I16,
430 TensorElementType::Int32 => DataType::I32,
431 TensorElementType::Int64 => DataType::I64,
432 TensorElementType::Float16 => DataType::F16,
433 TensorElementType::Bfloat16 => DataType::BF16,
434 TensorElementType::Bool => DataType::BOOL,
435 _ => return None,
436 })
437}
438
439fn to_ort_tensor(name: &str, tensor: &TensorBuffer) -> AiResult<DynValue> {
440 let descriptor = tensor.descriptor();
441 if !descriptor.is_c_contiguous() {
442 return Err(AiError::Unsupported {
443 backend: BACKEND_NAME.into(),
444 operation: format!("strided input `{name}`; explicitly pack it first"),
445 });
446 }
447 let range = descriptor.required_byte_range().map_err(|error| AiError::Backend {
448 backend: BACKEND_NAME.into(),
449 message: error.to_string(),
450 })?;
451 let bytes = &tensor.allocation_bytes()[range];
452 let shape = descriptor.shape().to_vec();
453 macro_rules! typed {
454 ($type:ty, $width:expr) => {{
455 let values = decode_ne::<$type>(bytes, $width, |chunk| {
456 <$type>::from_ne_bytes(chunk.try_into().expect("fixed chunk width"))
457 });
458 Tensor::<$type>::from_array((shape, values)).map(|value| value.into_dyn())
459 }};
460 }
461 let value = match descriptor.dtype() {
462 DataType::U8 => Tensor::<u8>::from_array((shape, bytes.to_vec())).map(|v| v.into_dyn()),
463 DataType::U16 => typed!(u16, 2),
464 DataType::U32 => typed!(u32, 4),
465 DataType::I8 => Tensor::<i8>::from_array((
466 shape,
467 bytes.iter().map(|&value| value as i8).collect::<Vec<_>>(),
468 ))
469 .map(|v| v.into_dyn()),
470 DataType::I16 => typed!(i16, 2),
471 DataType::I32 => typed!(i32, 4),
472 DataType::I64 => typed!(i64, 8),
473 DataType::F32 => typed!(f32, 4),
474 DataType::F64 => typed!(f64, 8),
475 dtype => {
476 return Err(AiError::Unsupported {
477 backend: BACKEND_NAME.into(),
478 operation: format!("input dtype {dtype:?}"),
479 })
480 }
481 };
482 value.map_err(ort_error)
483}
484
485fn decode_ne<T>(bytes: &[u8], width: usize, decode: impl Fn(&[u8]) -> T) -> Vec<T> {
486 debug_assert_eq!(bytes.len() % width, 0);
487 bytes.chunks_exact(width).map(decode).collect()
488}
489
490fn copy_ort_output(spec: &TensorSpec, value: &DynValue) -> AiResult<TensorBuffer> {
491 macro_rules! extract {
492 ($type:ty, $constructor:ident) => {{
493 let (shape, values) = value.try_extract_tensor::<$type>().map_err(ort_error)?;
494 let descriptor = TensorDescriptor::contiguous(
495 spec.dtype,
496 shape.iter().map(|&dimension| dimension as usize).collect(),
497 Device::CPU,
498 );
499 TensorBuffer::$constructor(values.to_vec(), descriptor)
500 }};
501 }
502 let result = match spec.dtype {
503 DataType::U8 => extract!(u8, try_new),
504 DataType::U16 => extract!(u16, try_from_u16),
505 DataType::U32 => extract!(u32, try_from_u32),
506 DataType::I8 => {
507 let (shape, values) = value.try_extract_tensor::<i8>().map_err(ort_error)?;
508 let descriptor = TensorDescriptor::contiguous(
509 DataType::I8,
510 shape.iter().map(|&dimension| dimension as usize).collect(),
511 Device::CPU,
512 );
513 TensorBuffer::try_new(values.iter().map(|&item| item as u8).collect(), descriptor)
514 }
515 DataType::I16 => extract!(i16, try_from_i16),
516 DataType::I32 => extract!(i32, try_from_i32),
517 DataType::I64 => extract!(i64, try_from_i64),
518 DataType::F32 => extract!(f32, try_from_f32),
519 DataType::F64 => extract!(f64, try_from_f64),
520 dtype => {
521 return Err(AiError::Unsupported {
522 backend: BACKEND_NAME.into(),
523 operation: format!("output dtype {dtype:?}"),
524 })
525 }
526 };
527 result.map_err(|error| AiError::Backend {
528 backend: BACKEND_NAME.into(),
529 message: error.to_string(),
530 })
531}
532
533fn ort_error(error: impl std::fmt::Display) -> AiError {
534 AiError::Backend { backend: BACKEND_NAME.into(), message: error.to_string() }
535}
536
537#[cfg(test)]
538mod tests {
539 use std::sync::Arc;
540
541 use super::OnnxRuntimeBackend;
542 use crate::{
543 AiError, CopyPolicy, Dimension, InferenceBackend, IoBinding, ModelSource, NamedTensors,
544 OutputBinding, PreallocatedOutput, RunOptions, SessionOptions,
545 };
546 use spatialrust_tensor::{DataType, Device, TensorBuffer, TensorDescriptor};
547
548 const DOUBLE_DYNAMIC: &[u8] = &[
549 8, 8, 18, 16, 115, 112, 97, 116, 105, 97, 108, 114, 117, 115, 116, 45, 116, 101, 115, 116,
550 58, 106, 10, 27, 10, 5, 105, 110, 112, 117, 116, 10, 5, 105, 110, 112, 117, 116, 18, 6,
551 111, 117, 116, 112, 117, 116, 34, 3, 65, 100, 100, 18, 14, 100, 111, 117, 98, 108, 101, 95,
552 100, 121, 110, 97, 109, 105, 99, 90, 28, 10, 5, 105, 110, 112, 117, 116, 18, 19, 10, 17, 8,
553 1, 18, 13, 10, 7, 18, 5, 98, 97, 116, 99, 104, 10, 2, 8, 3, 98, 29, 10, 6, 111, 117, 116,
554 112, 117, 116, 18, 19, 10, 17, 8, 1, 18, 13, 10, 7, 18, 5, 98, 97, 116, 99, 104, 10, 2, 8,
555 3, 66, 4, 10, 0, 16, 13,
556 ];
557
558 fn f32_tensor(shape: Vec<usize>, values: &[f32]) -> TensorBuffer {
559 TensorBuffer::try_from_f32(
560 values.to_vec(),
561 TensorDescriptor::contiguous(DataType::F32, shape, Device::CPU),
562 )
563 .unwrap()
564 }
565
566 fn f32_values(tensor: &TensorBuffer) -> Vec<f32> {
567 tensor
568 .allocation_bytes()
569 .chunks_exact(4)
570 .map(|chunk| f32::from_ne_bytes(chunk.try_into().unwrap()))
571 .collect()
572 }
573
574 fn dynamic_identity_model_with_onnx_dtype(dtype: u8) -> Arc<[u8]> {
575 let mut model = DOUBLE_DYNAMIC.to_vec();
576 let graph = model
577 .windows(4)
578 .position(|window| window == [58, 106, 10, 27])
579 .expect("embedded graph and node lengths");
580 model[graph + 1] = 104;
581 let node = graph + 2;
582 let identity_node = [
583 10, 25, 10, 5, 105, 110, 112, 117, 116, 18, 6, 111, 117, 116, 112, 117, 116, 34, 8, 73,
584 100, 101, 110, 116, 105, 116, 121,
585 ];
586 model.splice(node..node + 29, identity_node);
587 let mut replacements = 0;
588 for index in 0..model.len().saturating_sub(3) {
589 if model[index..index + 4] == [8, 1, 18, 13] {
590 model[index + 1] = dtype;
591 replacements += 1;
592 }
593 }
594 assert_eq!(replacements, 2, "input and output tensor types must both be replaced");
595 Arc::from(model)
596 }
597
598 #[test]
599 fn cpu_session_preserves_named_dynamic_contract_and_requires_copy_opt_in() {
600 let backend = OnnxRuntimeBackend;
601 let mut session = backend
602 .create_session(
603 &ModelSource::Bytes(Arc::from(DOUBLE_DYNAMIC)),
604 &SessionOptions::default(),
605 )
606 .unwrap();
607 assert_eq!(session.model_info().inputs[0].name, "input");
608 assert_eq!(
609 session.model_info().inputs[0].shape,
610 vec![Dimension::Symbol("batch".into()), Dimension::Fixed(3)]
611 );
612
613 let mut inputs = NamedTensors::new();
614 inputs.insert("input", f32_tensor(vec![2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0])).unwrap();
615 assert!(matches!(session.run(inputs.clone()), Err(AiError::CopyRequired { .. })));
616 let outputs = session
617 .run_with_options(
618 inputs,
619 RunOptions { input_copy: CopyPolicy::Allow, output_copy: CopyPolicy::Allow },
620 )
621 .unwrap();
622 let output = outputs.get("output").unwrap();
623 assert_eq!(output.descriptor().shape(), &[2, 3]);
624 assert_eq!(f32_values(output), &[2.0, 4.0, 6.0, 8.0, 10.0, 12.0]);
625 }
626
627 #[test]
628 fn io_binding_allocates_dynamic_output_and_reuses_it_as_zero_copy_input() {
629 let backend = OnnxRuntimeBackend;
630 let mut session = backend
631 .create_session(
632 &ModelSource::Bytes(Arc::from(DOUBLE_DYNAMIC)),
633 &SessionOptions::default(),
634 )
635 .unwrap();
636 let mut inputs = NamedTensors::new();
637 inputs.insert("input", f32_tensor(vec![1, 3], &[1.0, 2.0, 3.0])).unwrap();
638 let mut binding = IoBinding::try_new(
639 inputs,
640 vec![OutputBinding::Allocate { name: "output".into(), device: Device::CPU }],
641 )
642 .unwrap();
643 session.run_with_binding(&mut binding).unwrap();
644 let first = binding.results().unwrap().get("output").unwrap();
645 assert!(first.host_storage().is_some());
646 assert_eq!(f32_values(first), &[2.0, 4.0, 6.0]);
647
648 let mut inputs = NamedTensors::new();
649 inputs.insert("input", first.clone()).unwrap();
650 let mut chained = IoBinding::try_new(
651 inputs,
652 vec![OutputBinding::Allocate { name: "output".into(), device: Device::CPU }],
653 )
654 .unwrap();
655 session.run_with_binding(&mut chained).unwrap();
656 assert_eq!(
657 f32_values(chained.results().unwrap().get("output").unwrap()),
658 &[4.0, 8.0, 12.0]
659 );
660 }
661
662 #[test]
663 fn io_binding_writes_directly_into_caller_preallocated_f32_storage() {
664 let backend = OnnxRuntimeBackend;
665 let mut session = backend
666 .create_session(
667 &ModelSource::Bytes(Arc::from(DOUBLE_DYNAMIC)),
668 &SessionOptions::default(),
669 )
670 .unwrap();
671 let mut inputs = NamedTensors::new();
672 inputs.insert("input", f32_tensor(vec![2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0])).unwrap();
673 let descriptor = TensorDescriptor::contiguous(DataType::F32, vec![2, 3], Device::CPU);
674 let mut output = PreallocatedOutput::allocate("output", descriptor).unwrap();
675 let allocation = output.allocation_bytes_mut().unwrap().as_ptr();
676 let mut binding =
677 IoBinding::try_new(inputs, vec![OutputBinding::PreallocatedCpu(output)]).unwrap();
678 session.run_with_binding(&mut binding).unwrap();
679 let result = binding.results().unwrap().get("output").unwrap();
680 assert_eq!(result.allocation_bytes().as_ptr(), allocation);
681 assert_eq!(f32_values(result), &[2.0, 4.0, 6.0, 8.0, 10.0, 12.0]);
682 }
683
684 #[test]
685 fn io_binding_rejects_unaligned_raw_f32_storage_instead_of_copying() {
686 let backend = OnnxRuntimeBackend;
687 let mut session = backend
688 .create_session(
689 &ModelSource::Bytes(Arc::from(DOUBLE_DYNAMIC)),
690 &SessionOptions::default(),
691 )
692 .unwrap();
693 let descriptor = TensorDescriptor::contiguous(DataType::F32, vec![1, 3], Device::CPU);
694 let input = TensorBuffer::try_new(vec![0; 12], descriptor).unwrap();
695 let mut inputs = NamedTensors::new();
696 inputs.insert("input", input).unwrap();
697 let mut binding = IoBinding::try_new(
698 inputs,
699 vec![OutputBinding::Allocate { name: "output".into(), device: Device::CPU }],
700 )
701 .unwrap();
702 assert!(matches!(
703 session.run_with_binding(&mut binding),
704 Err(AiError::CopyRequired { direction: "input alignment", .. })
705 ));
706 }
707
708 #[test]
709 fn io_binding_supports_aligned_u8_and_u16_storage() {
710 let backend = OnnxRuntimeBackend;
711
712 let mut u8_session = backend
713 .create_session(
714 &ModelSource::Bytes(dynamic_identity_model_with_onnx_dtype(2)),
715 &SessionOptions::default(),
716 )
717 .unwrap();
718 let mut u8_inputs = NamedTensors::new();
719 u8_inputs
720 .insert(
721 "input",
722 TensorBuffer::try_new(
723 vec![1, 2, 3],
724 TensorDescriptor::contiguous(DataType::U8, vec![1, 3], Device::CPU),
725 )
726 .unwrap(),
727 )
728 .unwrap();
729 let mut u8_binding = IoBinding::try_new(
730 u8_inputs,
731 vec![OutputBinding::Allocate { name: "output".into(), device: Device::CPU }],
732 )
733 .unwrap();
734 u8_session.run_with_binding(&mut u8_binding).unwrap();
735 assert_eq!(
736 u8_binding.results().unwrap().get("output").unwrap().allocation_bytes(),
737 &[1, 2, 3]
738 );
739
740 let mut u16_session = backend
741 .create_session(
742 &ModelSource::Bytes(dynamic_identity_model_with_onnx_dtype(4)),
743 &SessionOptions::default(),
744 )
745 .unwrap();
746 let descriptor = TensorDescriptor::contiguous(DataType::U16, vec![1, 3], Device::CPU);
747 let mut u16_inputs = NamedTensors::new();
748 u16_inputs
749 .insert(
750 "input",
751 TensorBuffer::try_from_u16(vec![10, 20, 30], descriptor.clone()).unwrap(),
752 )
753 .unwrap();
754 let output = PreallocatedOutput::allocate("output", descriptor).unwrap();
755 let mut u16_binding =
756 IoBinding::try_new(u16_inputs, vec![OutputBinding::PreallocatedCpu(output)]).unwrap();
757 u16_session.run_with_binding(&mut u16_binding).unwrap();
758 assert_eq!(
759 bytemuck::cast_slice::<u8, u16>(
760 u16_binding.results().unwrap().get("output").unwrap().allocation_bytes()
761 ),
762 &[10, 20, 30]
763 );
764 }
765}