1use std::sync::{Arc, OnceLock};
27
28use onnx_runtime_ep_api::{DeviceBuffer, ExecutionProvider};
29use onnx_runtime_ep_cpu::CpuExecutionProvider;
30use onnx_runtime_ir::{DataType, DeviceId, TensorLayout};
31
32use crate::error::{Result, SessionError};
33
34pub(crate) fn shared_cpu_ep() -> Arc<CpuExecutionProvider> {
38 static EP: OnceLock<Arc<CpuExecutionProvider>> = OnceLock::new();
39 EP.get_or_init(|| {
40 let mut ep = CpuExecutionProvider::new();
41 let _ = ep.initialize(&Default::default());
43 Arc::new(ep)
44 })
45 .clone()
46}
47
48pub fn cpu_allocator() -> Arc<dyn ExecutionProvider> {
55 shared_cpu_ep()
56}
57
58pub(crate) struct SharedTensorBuffer {
65 buffer: Option<DeviceBuffer>,
66 allocator: Arc<dyn ExecutionProvider>,
67 import_guard: Option<Box<dyn core::any::Any + Send + Sync>>,
68}
69
70impl SharedTensorBuffer {
71 pub(crate) fn new(allocator: Arc<dyn ExecutionProvider>, buffer: DeviceBuffer) -> Arc<Self> {
72 Arc::new(Self {
73 buffer: Some(buffer),
74 allocator,
75 import_guard: None,
76 })
77 }
78
79 fn with_guard(
80 allocator: Arc<dyn ExecutionProvider>,
81 buffer: DeviceBuffer,
82 import_guard: Option<Box<dyn core::any::Any + Send + Sync>>,
83 ) -> Arc<Self> {
84 Arc::new(Self {
85 buffer: Some(buffer),
86 allocator,
87 import_guard,
88 })
89 }
90
91 pub(crate) fn allocate_cpu(bytes: usize) -> Result<Arc<Self>> {
92 let allocator: Arc<dyn ExecutionProvider> = shared_cpu_ep();
93 let buffer = allocator.allocate(bytes.max(1), TensorLayout::contiguous().alignment)?;
94 Ok(Self::new(allocator, buffer))
95 }
96
97 pub(crate) fn buffer(&self) -> &DeviceBuffer {
98 self.buffer
99 .as_ref()
100 .expect("SharedTensorBuffer buffer taken only in Drop")
101 }
102
103 pub(crate) fn buffer_mut(&mut self) -> &mut DeviceBuffer {
104 self.buffer
105 .as_mut()
106 .expect("SharedTensorBuffer buffer taken only in Drop")
107 }
108
109 pub(crate) fn allocator(&self) -> &Arc<dyn ExecutionProvider> {
110 &self.allocator
111 }
112
113 pub(crate) fn alias(&self) -> DeviceBuffer {
116 let buffer = self.buffer();
117 unsafe {
121 DeviceBuffer::from_borrowed_parts(
122 buffer.as_ptr() as *mut std::ffi::c_void,
123 buffer.device(),
124 buffer.len(),
125 buffer.alignment(),
126 )
127 }
128 }
129
130 pub(crate) fn into_buffer(mut self) -> DeviceBuffer {
131 debug_assert!(
132 self.import_guard.is_none(),
133 "executor-promoted buffers never carry a foreign import guard"
134 );
135 self.buffer
136 .take()
137 .expect("SharedTensorBuffer buffer taken only by into_buffer or Drop")
138 }
139}
140
141impl std::fmt::Debug for SharedTensorBuffer {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 f.debug_struct("SharedTensorBuffer")
144 .field("device", &self.buffer().device())
145 .field("len", &self.buffer().len())
146 .field("ptr", &self.buffer().as_ptr())
147 .finish()
148 }
149}
150
151impl Drop for SharedTensorBuffer {
152 fn drop(&mut self) {
153 if let Some(buffer) = self.buffer.take() {
154 let _ = self.allocator.deallocate(buffer);
155 }
156 let _ = self.import_guard.take();
157 }
158}
159
160pub(crate) fn host_bytes(buffer: &DeviceBuffer) -> &[u8] {
170 assert!(
171 buffer.device().is_host_accessible(),
172 "host_bytes on non-host device {:?}",
173 buffer.device()
174 );
175 if buffer.is_empty() {
176 return &[];
177 }
178 unsafe { std::slice::from_raw_parts(buffer.as_ptr() as *const u8, buffer.len()) }
183}
184
185pub struct Tensor {
192 pub dtype: DataType,
194 pub shape: Vec<usize>,
196 pub layout: TensorLayout,
199 device: DeviceId,
200 buffer: Option<DeviceBuffer>,
202 allocator: Arc<dyn ExecutionProvider>,
204 import_guard: Option<Box<dyn core::any::Any + Send + Sync>>,
215}
216
217#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
220pub struct DeviceBindingTransferStats {
221 pub host_upload_calls: u64,
222 pub host_upload_bytes: u64,
223 pub host_download_calls: u64,
224 pub host_download_bytes: u64,
225}
226
227pub struct DeviceIoBinding {
230 input_name: String,
231 bind_input: bool,
232 output_name: Option<String>,
233 pub dtype: DataType,
234 physical_shape: Vec<usize>,
235 logical_shape: Vec<usize>,
236 expose_logical_input_shape: bool,
238 buffer: Option<DeviceBuffer>,
239 allocator: Arc<dyn ExecutionProvider>,
240 transfer_stats: DeviceBindingTransferStats,
241}
242
243impl DeviceIoBinding {
244 pub(crate) fn allocate(
245 allocator: Arc<dyn ExecutionProvider>,
246 input_name: String,
247 bind_input: bool,
248 output_name: Option<String>,
249 dtype: DataType,
250 physical_shape: Vec<usize>,
251 logical_shape: Vec<usize>,
252 expose_logical_input_shape: bool,
253 ) -> Result<Self> {
254 validate_logical_shape(&physical_shape, &logical_shape)?;
255 let numel = physical_shape.iter().try_fold(1usize, |product, &dim| {
256 product.checked_mul(dim).ok_or_else(|| {
257 SessionError::Internal(format!(
258 "device binding '{input_name}' physical shape overflows: {physical_shape:?}"
259 ))
260 })
261 })?;
262 let bytes = dtype.storage_bytes(numel).max(1);
263 let allocator_for_buffer = allocator.clone();
264 let buffer = allocator_for_buffer.allocate(bytes, TensorLayout::contiguous().alignment)?;
265 Ok(Self {
266 input_name,
267 bind_input,
268 output_name,
269 dtype,
270 physical_shape,
271 logical_shape,
272 expose_logical_input_shape,
273 buffer: Some(buffer),
274 allocator,
275 transfer_stats: DeviceBindingTransferStats::default(),
276 })
277 }
278
279 pub fn input_name(&self) -> &str {
280 &self.input_name
281 }
282
283 pub(crate) fn binds_input(&self) -> bool {
284 self.bind_input
285 }
286
287 pub fn output_name(&self) -> Option<&str> {
288 self.output_name.as_deref()
289 }
290
291 pub fn physical_shape(&self) -> &[usize] {
292 &self.physical_shape
293 }
294
295 pub fn logical_shape(&self) -> &[usize] {
296 &self.logical_shape
297 }
298
299 pub(crate) fn kernel_input_shape(&self) -> &[usize] {
300 if self.expose_logical_input_shape {
301 &self.logical_shape
302 } else {
303 &self.physical_shape
304 }
305 }
306
307 pub fn has_dynamic_logical_input_shape(&self) -> bool {
309 self.bind_input
310 && self.expose_logical_input_shape
311 && self.logical_shape != self.physical_shape
312 }
313
314 pub fn exposes_logical_input_shape(&self) -> bool {
324 self.bind_input && self.expose_logical_input_shape
325 }
326
327 pub fn set_logical_shape(&mut self, shape: Vec<usize>) -> Result<()> {
328 validate_logical_shape(&self.physical_shape, &shape)?;
329 self.logical_shape = shape;
330 Ok(())
331 }
332
333 pub fn device_ptr(&self) -> *const std::ffi::c_void {
334 self.buffer().as_ptr()
335 }
336
337 pub fn transfer_stats(&self) -> DeviceBindingTransferStats {
338 self.transfer_stats
339 }
340
341 pub fn write_bytes(&mut self, byte_offset: usize, bytes: &[u8]) -> Result<()> {
342 let buffer = self
343 .buffer
344 .as_mut()
345 .expect("DeviceIoBinding buffer taken only in Drop");
346 self.allocator
347 .copy_from_host_at(bytes, buffer, byte_offset)?;
348 self.transfer_stats.host_upload_calls += 1;
349 self.transfer_stats.host_upload_bytes += bytes.len() as u64;
350 Ok(())
351 }
352
353 pub fn read_bytes(&mut self) -> Result<Vec<u8>> {
354 let mut bytes = vec![0; self.buffer().len()];
355 self.read_bytes_into(&mut bytes)?;
356 Ok(bytes)
357 }
358
359 pub fn read_bytes_into(&mut self, bytes: &mut [u8]) -> Result<()> {
360 self.allocator.copy_to_host(self.buffer(), bytes)?;
361 self.transfer_stats.host_download_calls += 1;
362 self.transfer_stats.host_download_bytes += bytes.len() as u64;
363 Ok(())
364 }
365
366 pub fn device_argmax_supported(&self) -> bool {
367 self.allocator.device_argmax_supported()
368 }
369
370 pub fn device_argmax(&self, elements: usize, result: &mut DeviceIoBinding) -> Result<()> {
371 if !matches!(self.dtype, DataType::Float32 | DataType::Float16)
372 || result.dtype != DataType::Uint32
373 {
374 return Err(SessionError::Internal(format!(
375 "device argmax requires f32/f16 logits and u32 result, got {:?} and {:?}",
376 self.dtype, result.dtype
377 )));
378 }
379 if !Arc::ptr_eq(&self.allocator, &result.allocator) {
380 return Err(SessionError::Internal(
381 "device argmax bindings must belong to the same execution provider".into(),
382 ));
383 }
384 Ok(self.allocator.device_argmax(
385 self.buffer(),
386 elements,
387 self.dtype,
388 result.buffer_mut(),
389 )?)
390 }
391
392 pub(crate) fn buffer(&self) -> &DeviceBuffer {
393 self.buffer
394 .as_ref()
395 .expect("DeviceIoBinding buffer taken only in Drop")
396 }
397
398 pub(crate) fn buffer_mut(&mut self) -> &mut DeviceBuffer {
399 self.buffer
400 .as_mut()
401 .expect("DeviceIoBinding buffer taken only in Drop")
402 }
403}
404
405fn validate_logical_shape(physical: &[usize], logical: &[usize]) -> Result<()> {
406 if physical.len() != logical.len()
407 || physical
408 .iter()
409 .zip(logical)
410 .any(|(&capacity, &valid)| valid > capacity)
411 {
412 return Err(SessionError::Internal(format!(
413 "device binding logical shape {logical:?} exceeds physical capacity {physical:?}"
414 )));
415 }
416 Ok(())
417}
418
419impl std::fmt::Debug for DeviceIoBinding {
420 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421 f.debug_struct("DeviceIoBinding")
422 .field("input_name", &self.input_name)
423 .field("bind_input", &self.bind_input)
424 .field("output_name", &self.output_name)
425 .field("dtype", &self.dtype)
426 .field("physical_shape", &self.physical_shape)
427 .field("logical_shape", &self.logical_shape)
428 .field(
429 "expose_logical_input_shape",
430 &self.expose_logical_input_shape,
431 )
432 .field("device", &self.buffer().device())
433 .field("device_ptr", &self.device_ptr())
434 .field("transfer_stats", &self.transfer_stats)
435 .finish()
436 }
437}
438
439impl Drop for DeviceIoBinding {
440 fn drop(&mut self) {
441 if let Some(buffer) = self.buffer.take() {
442 let _ = self.allocator.reset_device_graph();
443 let _ = self.allocator.deallocate(buffer);
444 }
445 }
446}
447
448impl Tensor {
449 pub(crate) fn allocate_cpu(dtype: DataType, shape: Vec<usize>) -> Result<Self> {
450 let numel = shape.iter().try_fold(1usize, |product, &dim| {
451 product.checked_mul(dim).ok_or_else(|| {
452 SessionError::Internal(format!(
453 "Tensor::allocate_cpu: element count overflows for shape {shape:?}"
454 ))
455 })
456 })?;
457 let bytes = dtype.checked_storage_bytes(numel).ok_or_else(|| {
458 SessionError::Internal(format!(
459 "Tensor::allocate_cpu: byte count overflows for shape {shape:?} dtype {dtype:?}"
460 ))
461 })?;
462 let allocator: Arc<dyn ExecutionProvider> = shared_cpu_ep();
463 let layout = TensorLayout::contiguous();
464 let buffer = allocator.allocate(bytes.max(1), layout.alignment)?;
465 Ok(Self {
466 dtype,
467 shape,
468 layout,
469 device: buffer.device(),
470 buffer: Some(buffer),
471 allocator,
472 import_guard: None,
473 })
474 }
475
476 pub(crate) fn copy_from_host_at(&mut self, offset: usize, bytes: &[u8]) -> Result<()> {
477 let buffer = self.buffer.as_mut().ok_or_else(|| {
478 SessionError::Internal("Tensor buffer is unavailable for writing".to_string())
479 })?;
480 self.allocator.copy_from_host_at(bytes, buffer, offset)?;
481 Ok(())
482 }
483
484 pub(crate) fn from_raw_in(
489 allocator: Arc<dyn ExecutionProvider>,
490 dtype: DataType,
491 shape: Vec<usize>,
492 bytes: &[u8],
493 ) -> Result<Self> {
494 let numel: usize = shape.iter().product();
495 let expected = dtype.storage_bytes(numel);
496 if bytes.len() != expected {
497 return Err(SessionError::Internal(format!(
498 "Tensor::from_raw_in: {} bytes for shape {shape:?} dtype {dtype:?}, expected {expected}",
499 bytes.len()
500 )));
501 }
502 let layout = TensorLayout::contiguous();
503 let align = layout.alignment;
504 let mut buffer = allocator.allocate(expected.max(1), align)?;
505 allocator.copy_from_host(bytes, &mut buffer)?;
506 Ok(Self {
507 dtype,
508 shape,
509 layout,
510 device: buffer.device(),
511 buffer: Some(buffer),
512 allocator,
513 import_guard: None,
514 })
515 }
516
517 pub fn from_raw(dtype: DataType, shape: Vec<usize>, bytes: &[u8]) -> Result<Self> {
519 Self::from_raw_in(shared_cpu_ep(), dtype, shape, bytes)
520 }
521
522 pub(crate) fn from_owned_buffer(
534 allocator: Arc<dyn ExecutionProvider>,
535 dtype: DataType,
536 shape: Vec<usize>,
537 buffer: DeviceBuffer,
538 ) -> Self {
539 debug_assert!(
540 !buffer.is_borrowed(),
541 "from_owned_buffer requires an owned buffer"
542 );
543 debug_assert_eq!(
544 buffer.len(),
545 dtype.storage_bytes(shape.iter().product::<usize>()).max(1),
546 "from_owned_buffer size mismatch for shape {shape:?} dtype {dtype:?}",
547 );
548 let device = buffer.device();
549 Self {
550 dtype,
551 shape,
552 layout: TensorLayout::contiguous(),
553 device,
554 buffer: Some(buffer),
555 allocator,
556 import_guard: None,
557 }
558 }
559
560 pub fn from_f32(shape: &[usize], data: &[f32]) -> Result<Self> {
562 let mut bytes = Vec::with_capacity(data.len() * 4);
563 for v in data {
564 bytes.extend_from_slice(&v.to_le_bytes());
565 }
566 Self::from_raw(DataType::Float32, shape.to_vec(), &bytes)
567 }
568
569 pub fn from_i64(shape: &[usize], data: &[i64]) -> Result<Self> {
571 let mut bytes = Vec::with_capacity(data.len() * 8);
572 for v in data {
573 bytes.extend_from_slice(&v.to_le_bytes());
574 }
575 Self::from_raw(DataType::Int64, shape.to_vec(), &bytes)
576 }
577
578 pub(crate) fn into_shared_parts(
579 mut self,
580 ) -> (Arc<SharedTensorBuffer>, DataType, Vec<usize>, TensorLayout) {
581 let buffer = self
582 .buffer
583 .take()
584 .expect("Tensor buffer taken only by into_shared_parts or Drop");
585 let storage = SharedTensorBuffer::with_guard(
586 Arc::clone(&self.allocator),
587 buffer,
588 self.import_guard.take(),
589 );
590 let dtype = self.dtype;
591 let shape = std::mem::take(&mut self.shape);
592 let layout = std::mem::take(&mut self.layout);
593 (storage, dtype, shape, layout)
594 }
595
596 pub fn device(&self) -> DeviceId {
598 self.device
599 }
600
601 pub fn from_borrowed_parts_with_guard(
625 allocator: Arc<dyn ExecutionProvider>,
626 dtype: DataType,
627 shape: Vec<usize>,
628 layout: TensorLayout,
629 buffer: DeviceBuffer,
630 guard: Box<dyn core::any::Any + Send + Sync>,
631 ) -> Self {
632 debug_assert!(
633 buffer.is_borrowed(),
634 "from_borrowed_parts_with_guard requires a borrowed DeviceBuffer; \
635 an owned buffer would be freed twice (EP deallocate + guard)"
636 );
637 Self {
638 dtype,
639 shape,
640 layout,
641 device: buffer.device(),
642 buffer: Some(buffer),
643 allocator,
644 import_guard: Some(guard),
645 }
646 }
647
648 pub fn numel(&self) -> usize {
650 self.shape.iter().product()
651 }
652
653 pub fn device_ptr(&self) -> *const std::ffi::c_void {
663 if self.numel() == 0 {
664 std::ptr::null()
665 } else {
666 self.buffer().as_ptr()
667 }
668 }
669
670 pub fn sync(&self) -> Result<()> {
679 self.allocator.sync()?;
680 Ok(())
681 }
682
683 fn buffer(&self) -> &DeviceBuffer {
684 self.buffer
685 .as_ref()
686 .expect("Tensor buffer taken only in Drop")
687 }
688
689 pub fn as_bytes(&self) -> &[u8] {
691 let n = self.dtype.storage_bytes(self.numel());
692 &host_bytes(self.buffer())[..n]
693 }
694
695 pub(crate) fn overwrite_bytes(&mut self, bytes: &[u8]) -> Result<()> {
699 let expected = self.dtype.storage_bytes(self.numel());
700 if bytes.len() != expected {
701 return Err(SessionError::Internal(format!(
702 "Tensor::overwrite_bytes: got {} bytes for shape {:?} dtype {:?}, expected {expected}",
703 bytes.len(),
704 self.shape,
705 self.dtype
706 )));
707 }
708 let buffer = self
709 .buffer
710 .as_mut()
711 .expect("Tensor buffer taken only in Drop");
712 self.allocator.copy_from_host(bytes, buffer)?;
713 Ok(())
714 }
715
716 pub fn try_as_slice_f32(&self) -> Option<&[f32]> {
720 assert_eq!(
721 self.dtype,
722 DataType::Float32,
723 "try_as_slice_f32 on non-f32 tensor"
724 );
725 if cfg!(target_endian = "big") {
726 return None;
727 }
728 let bytes = self.as_bytes();
729 if bytes.as_ptr().align_offset(std::mem::align_of::<f32>()) != 0 {
730 return None;
731 }
732 Some(unsafe { std::slice::from_raw_parts(bytes.as_ptr().cast::<f32>(), self.numel()) })
735 }
736
737 pub fn try_as_slice_u16(&self) -> Option<&[u16]> {
740 assert!(
741 matches!(self.dtype, DataType::Float16 | DataType::BFloat16),
742 "try_as_slice_u16 on non-16-bit float tensor"
743 );
744 if cfg!(target_endian = "big") {
745 return None;
746 }
747 let bytes = self.as_bytes();
748 if bytes.as_ptr().align_offset(std::mem::align_of::<u16>()) != 0 {
749 return None;
750 }
751 Some(unsafe { std::slice::from_raw_parts(bytes.as_ptr().cast::<u16>(), self.numel()) })
754 }
755
756 pub fn to_vec_f32(&self) -> Vec<f32> {
758 assert_eq!(
759 self.dtype,
760 DataType::Float32,
761 "to_vec_f32 on non-f32 tensor"
762 );
763 self.try_as_slice_f32().map_or_else(
764 || {
765 self.as_bytes()
766 .chunks_exact(4)
767 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
768 .collect()
769 },
770 <[f32]>::to_vec,
771 )
772 }
773
774 pub fn to_vec_i64(&self) -> Vec<i64> {
776 assert_eq!(self.dtype, DataType::Int64, "to_vec_i64 on non-i64 tensor");
777 self.as_bytes()
778 .chunks_exact(8)
779 .map(|c| i64::from_le_bytes(c.try_into().unwrap()))
780 .collect()
781 }
782}
783
784impl Clone for Tensor {
785 fn clone(&self) -> Self {
786 Self::from_raw_in(
790 self.allocator.clone(),
791 self.dtype,
792 self.shape.clone(),
793 self.as_bytes(),
794 )
795 .expect("Tensor::clone: re-allocation of identical bytes")
796 }
797}
798
799impl std::fmt::Debug for Tensor {
800 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
801 f.debug_struct("Tensor")
802 .field("dtype", &self.dtype)
803 .field("shape", &self.shape)
804 .field("device", &self.device)
805 .finish()
806 }
807}
808
809impl Drop for Tensor {
810 fn drop(&mut self) {
811 if let Some(buffer) = self.buffer.take() {
812 let _ = self.allocator.deallocate(buffer);
817 }
818 let _ = self.import_guard.take();
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831 use std::os::raw::c_void;
832 use std::sync::atomic::{AtomicUsize, Ordering};
833
834 struct CountingGuard(Arc<AtomicUsize>);
837 impl Drop for CountingGuard {
838 fn drop(&mut self) {
839 self.0.fetch_add(1, Ordering::SeqCst);
840 }
841 }
842
843 #[test]
844 fn borrowed_guard_ctor_runs_guard_exactly_once_on_drop() {
845 let drops = Arc::new(AtomicUsize::new(0));
846 let mut backing = [1.0f32, 2.0, 3.0, 4.0];
848 let ptr = backing.as_mut_ptr() as *mut c_void;
849 let buffer = unsafe {
851 DeviceBuffer::from_borrowed_parts(ptr, DeviceId::cpu(), backing.len() * 4, 4)
852 };
853 assert!(buffer.is_borrowed());
854
855 let guard = Box::new(CountingGuard(drops.clone()));
856 let tensor = Tensor::from_borrowed_parts_with_guard(
857 shared_cpu_ep(),
858 DataType::Float32,
859 vec![4],
860 TensorLayout::contiguous(),
861 buffer,
862 guard,
863 );
864
865 assert_eq!(tensor.as_bytes().len(), 16);
867 assert_eq!(tensor.try_as_slice_f32().unwrap(), &[1.0, 2.0, 3.0, 4.0]);
868 assert_eq!(tensor.to_vec_f32(), vec![1.0, 2.0, 3.0, 4.0]);
869 assert_eq!(
870 drops.load(Ordering::SeqCst),
871 0,
872 "guard alive while tensor is"
873 );
874
875 drop(tensor);
876 assert_eq!(
877 drops.load(Ordering::SeqCst),
878 1,
879 "guard runs exactly once on drop"
880 );
881 }
882
883 #[test]
884 fn borrows_aligned_half_storage_as_raw_bits() {
885 let bits = [0x3c00u16, 0x4000];
886 let bytes = bits
887 .iter()
888 .flat_map(|value| value.to_le_bytes())
889 .collect::<Vec<_>>();
890 let tensor = Tensor::from_raw(DataType::Float16, vec![2], &bytes).unwrap();
891 assert_eq!(tensor.try_as_slice_u16().unwrap(), bits);
892 }
893
894 #[test]
903 fn exposes_logical_is_static_while_dynamic_tracks_current_shape() {
904 let mut logical_mask = DeviceIoBinding::allocate(
907 shared_cpu_ep(),
908 "attention_mask".into(),
909 true,
910 None,
911 DataType::Int64,
912 vec![1, 4096],
913 vec![1, 4096],
914 true,
915 )
916 .unwrap();
917 assert!(logical_mask.exposes_logical_input_shape());
918 assert!(!logical_mask.has_dynamic_logical_input_shape());
920 logical_mask.set_logical_shape(vec![1, 5]).unwrap();
923 assert!(logical_mask.exposes_logical_input_shape());
924 assert!(logical_mask.has_dynamic_logical_input_shape());
925 assert_eq!(logical_mask.kernel_input_shape(), &[1, 5]);
926
927 let mut physical_mask = DeviceIoBinding::allocate(
931 shared_cpu_ep(),
932 "attention_mask".into(),
933 true,
934 None,
935 DataType::Int64,
936 vec![1, 4096],
937 vec![1, 5],
938 false,
939 )
940 .unwrap();
941 assert!(!physical_mask.exposes_logical_input_shape());
942 assert!(!physical_mask.has_dynamic_logical_input_shape());
943 assert_eq!(physical_mask.kernel_input_shape(), &[1, 4096]);
944 physical_mask.set_logical_shape(vec![1, 4096]).unwrap();
945 assert!(!physical_mask.exposes_logical_input_shape());
946 }
947}