Skip to main content

onnx_runtime_session/
tensor.rs

1//! The owned, device-aware [`Tensor`] handed to and returned from
2//! [`InferenceSession::run`](crate::InferenceSession::run), plus the isolated
3//! host-buffer accessors it and the executor share.
4//!
5//! ## Placement decision (design open question, §20 / plan §2.D)
6//!
7//! The plan flagged *where* the real tensor type should live as an open
8//! question: keep it in `onnx-runtime-session`, or hoist it into a shared
9//! `onnx-runtime-tensor` crate. For Phase 1 (CPU only) it lives **here**: the
10//! type is a thin owner over an [`onnx_runtime_ep_api::DeviceBuffer`] plus the
11//! IR vocabulary ([`DataType`], [`TensorLayout`], shape) — nothing CPU-specific
12//! leaks into its shape. When `ep-cuda` lands and non-host tensors need DLPack
13//! import/export and cross-device copies, this is a mechanical move into a
14//! shared crate that both the session and the C-API can depend on; nothing in
15//! its public surface here presumes a host device beyond the accessors, which
16//! already gate on [`DeviceId::is_host_accessible`].
17//!
18//! ## The single `unsafe` seam
19//!
20//! A [`DeviceBuffer`] hands out only raw base pointers; reading or writing the
21//! bytes is `unsafe` and sound only on host-accessible devices. Every such
22//! access in this crate funnels through [`host_bytes`] / [`write_host`] /
23//! [`copy_host`], which assert host accessibility and length, so the rest of
24//! the crate — the executor and the public API — is safe Rust over the EP
25//! contract.
26
27use std::sync::{Arc, OnceLock};
28
29use onnx_runtime_ep_api::{DeviceBuffer, ExecutionProvider};
30use onnx_runtime_ep_cpu::CpuExecutionProvider;
31use onnx_runtime_ir::{DataType, DeviceId, TensorLayout};
32
33use crate::error::{Result, SessionError};
34
35/// A process-wide, already-initialized CPU execution provider used to back
36/// user-constructed [`Tensor`]s (host `malloc`/`free` is global, so any
37/// `CpuExecutionProvider` can free any other's CPU allocation).
38pub(crate) fn shared_cpu_ep() -> Arc<CpuExecutionProvider> {
39    static EP: OnceLock<Arc<CpuExecutionProvider>> = OnceLock::new();
40    EP.get_or_init(|| {
41        let mut ep = CpuExecutionProvider::new();
42        // Pure-Rust CPU EP: `initialize` only flips a flag and never fails.
43        let _ = ep.initialize(&Default::default());
44        Arc::new(ep)
45    })
46    .clone()
47}
48
49/// Borrow the raw bytes of a host-accessible device buffer.
50///
51/// # Safety
52///
53/// `buffer` must live on a host-accessible device (asserted) and own a valid
54/// allocation of `buffer.len()` bytes (the EP contract for
55/// [`DeviceBuffer`]). The returned slice borrows `buffer`, so it cannot outlive
56/// it. No concurrent writer may exist for the borrow's duration — enforced by
57/// the `&DeviceBuffer` shared borrow in safe code.
58pub(crate) fn host_bytes(buffer: &DeviceBuffer) -> &[u8] {
59    assert!(
60        buffer.device().is_host_accessible(),
61        "host_bytes on non-host device {:?}",
62        buffer.device()
63    );
64    if buffer.is_empty() {
65        return &[];
66    }
67    // SAFETY: host-accessible device (asserted) means `as_ptr` is a real,
68    // readable host address; the EP guarantees `len()` valid bytes behind it.
69    // The lifetime is tied to `&buffer`, so the slice cannot dangle, and the
70    // shared borrow forbids an aliasing writer while it is live.
71    unsafe { std::slice::from_raw_parts(buffer.as_ptr() as *const u8, buffer.len()) }
72}
73
74/// Copy `src` into the front of a host-accessible device buffer.
75pub(crate) fn write_host(buffer: &mut DeviceBuffer, src: &[u8]) -> Result<()> {
76    assert!(
77        buffer.device().is_host_accessible(),
78        "write_host on non-host device {:?}",
79        buffer.device()
80    );
81    if src.len() > buffer.len() {
82        return Err(SessionError::Internal(format!(
83            "write_host: source {} bytes exceeds buffer {} bytes",
84            src.len(),
85            buffer.len()
86        )));
87    }
88    if src.is_empty() {
89        return Ok(());
90    }
91    let dst = buffer.as_mut_ptr() as *mut u8;
92    // SAFETY: host-accessible device (asserted); `dst` is a unique writable host
93    // pointer obtained via `&mut buffer` (no alias), with at least `src.len()`
94    // bytes of capacity (checked above). `src` is a distinct owned slice, so the
95    // ranges do not overlap.
96    unsafe {
97        std::ptr::copy_nonoverlapping(src.as_ptr(), dst, src.len());
98    }
99    Ok(())
100}
101
102/// An owned, host-resident, device-aware tensor (§5, §20.2).
103///
104/// Owns the [`DeviceBuffer`] that holds its elements and the EP that must free
105/// it. On Phase-1 CPU the buffer is a host allocation, so [`Tensor::as_bytes`]
106/// and the typed accessors read it directly; the design leaves room for
107/// non-host devices (the accessors gate on host accessibility).
108pub struct Tensor {
109    /// Element type.
110    pub dtype: DataType,
111    /// Logical shape (static dims).
112    pub shape: Vec<usize>,
113    /// Physical layout of [`Tensor::buffer`]. Row-major contiguous for tensors
114    /// this crate produces.
115    pub layout: TensorLayout,
116    device: DeviceId,
117    /// `Some` while the tensor is live; taken by [`Drop`] to free exactly once.
118    buffer: Option<DeviceBuffer>,
119    /// The EP that allocated [`Tensor::buffer`] and must deallocate it.
120    allocator: Arc<dyn ExecutionProvider>,
121}
122
123impl Tensor {
124    /// Allocate a tensor from raw little-endian element bytes using `allocator`.
125    ///
126    /// `bytes` must hold exactly `storage_bytes(numel)` bytes for `dtype` and
127    /// `shape`.
128    pub(crate) fn from_raw_in(
129        allocator: Arc<dyn ExecutionProvider>,
130        dtype: DataType,
131        shape: Vec<usize>,
132        bytes: &[u8],
133    ) -> Result<Self> {
134        let numel: usize = shape.iter().product();
135        let expected = dtype.storage_bytes(numel);
136        if bytes.len() != expected {
137            return Err(SessionError::Internal(format!(
138                "Tensor::from_raw_in: {} bytes for shape {shape:?} dtype {dtype:?}, expected {expected}",
139                bytes.len()
140            )));
141        }
142        let layout = TensorLayout::contiguous();
143        let align = layout.alignment;
144        let mut buffer = allocator.allocate(expected.max(1), align)?;
145        write_host(&mut buffer, bytes)?;
146        Ok(Self {
147            dtype,
148            shape,
149            layout,
150            device: buffer.device(),
151            buffer: Some(buffer),
152            allocator,
153        })
154    }
155
156    /// Build a tensor from raw little-endian bytes on the shared CPU device.
157    pub fn from_raw(dtype: DataType, shape: Vec<usize>, bytes: &[u8]) -> Result<Self> {
158        Self::from_raw_in(shared_cpu_ep(), dtype, shape, bytes)
159    }
160
161    /// Build an `f32` tensor from a dense row-major slice.
162    pub fn from_f32(shape: &[usize], data: &[f32]) -> Result<Self> {
163        let mut bytes = Vec::with_capacity(data.len() * 4);
164        for v in data {
165            bytes.extend_from_slice(&v.to_le_bytes());
166        }
167        Self::from_raw(DataType::Float32, shape.to_vec(), &bytes)
168    }
169
170    /// Build an `i64` tensor from a dense row-major slice.
171    pub fn from_i64(shape: &[usize], data: &[i64]) -> Result<Self> {
172        let mut bytes = Vec::with_capacity(data.len() * 8);
173        for v in data {
174            bytes.extend_from_slice(&v.to_le_bytes());
175        }
176        Self::from_raw(DataType::Int64, shape.to_vec(), &bytes)
177    }
178
179    /// The device this tensor lives on.
180    pub fn device(&self) -> DeviceId {
181        self.device
182    }
183
184    /// Number of elements.
185    pub fn numel(&self) -> usize {
186        self.shape.iter().product()
187    }
188
189    fn buffer(&self) -> &DeviceBuffer {
190        self.buffer
191            .as_ref()
192            .expect("Tensor buffer taken only in Drop")
193    }
194
195    /// Borrow the raw little-endian element bytes (host tensors only).
196    pub fn as_bytes(&self) -> &[u8] {
197        let n = self.dtype.storage_bytes(self.numel());
198        &host_bytes(self.buffer())[..n]
199    }
200
201    /// Copy out the elements as `f32`. Panics if the dtype is not `Float32`.
202    pub fn to_vec_f32(&self) -> Vec<f32> {
203        assert_eq!(self.dtype, DataType::Float32, "to_vec_f32 on non-f32 tensor");
204        self.as_bytes()
205            .chunks_exact(4)
206            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
207            .collect()
208    }
209
210    /// Copy out the elements as `i64`. Panics if the dtype is not `Int64`.
211    pub fn to_vec_i64(&self) -> Vec<i64> {
212        assert_eq!(self.dtype, DataType::Int64, "to_vec_i64 on non-i64 tensor");
213        self.as_bytes()
214            .chunks_exact(8)
215            .map(|c| i64::from_le_bytes(c.try_into().unwrap()))
216            .collect()
217    }
218}
219
220impl Clone for Tensor {
221    fn clone(&self) -> Self {
222        // Deep copy: a fresh allocation with identical bytes. Cannot fail for
223        // host allocations of the same size; propagate as a panic-free fallback
224        // by re-using `from_raw_in` and unwrapping the size-checked path.
225        Self::from_raw_in(
226            self.allocator.clone(),
227            self.dtype,
228            self.shape.clone(),
229            self.as_bytes(),
230        )
231        .expect("Tensor::clone: re-allocation of identical bytes")
232    }
233}
234
235impl std::fmt::Debug for Tensor {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        f.debug_struct("Tensor")
238            .field("dtype", &self.dtype)
239            .field("shape", &self.shape)
240            .field("device", &self.device)
241            .finish()
242    }
243}
244
245impl Drop for Tensor {
246    fn drop(&mut self) {
247        if let Some(buffer) = self.buffer.take() {
248            // `DeviceBuffer` has no `Drop`; the owning EP must free it exactly
249            // once (ep-api §4.4 invariant #2). Errors here cannot be surfaced
250            // from `drop`, so we swallow them — a failed free leaks, never
251            // double-frees.
252            let _ = self.allocator.deallocate(buffer);
253        }
254    }
255}