Skip to main content

tensor4all_tensorbackend/
logical_tensor.rs

1//! Backend-free logical tensor snapshots for transfer between execution domains.
2
3use num_complex::{Complex32, Complex64};
4use tenferro::{DType, Tensor, TensorRead};
5use tenferro_tensor::TensorDeviceTransfer;
6
7use crate::CpuExecutionContext;
8
9/// Owned column-major scalar payload for a [`LogicalTensor`].
10///
11/// The enum contains values only; it cannot carry an executor, backend,
12/// runtime, cache, pointer, or address identity.
13#[derive(Clone, Debug, PartialEq)]
14pub enum LogicalTensorData {
15    /// 32-bit real values.
16    F32(Vec<f32>),
17    /// 64-bit real values.
18    F64(Vec<f64>),
19    /// 32-bit signed integer values.
20    I32(Vec<i32>),
21    /// 64-bit signed integer values.
22    I64(Vec<i64>),
23    /// Boolean values.
24    Bool(Vec<bool>),
25    /// 32-bit complex values.
26    C32(Vec<Complex32>),
27    /// 64-bit complex values.
28    C64(Vec<Complex64>),
29}
30
31impl LogicalTensorData {
32    /// Return the scalar dtype represented by this payload.
33    pub fn dtype(&self) -> DType {
34        match self {
35            Self::F32(_) => DType::F32,
36            Self::F64(_) => DType::F64,
37            Self::I32(_) => DType::I32,
38            Self::I64(_) => DType::I64,
39            Self::Bool(_) => DType::Bool,
40            Self::C32(_) => DType::C32,
41            Self::C64(_) => DType::C64,
42        }
43    }
44
45    /// Return the number of logical scalar values.
46    pub fn len(&self) -> usize {
47        match self {
48            Self::F32(values) => values.len(),
49            Self::F64(values) => values.len(),
50            Self::I32(values) => values.len(),
51            Self::I64(values) => values.len(),
52            Self::Bool(values) => values.len(),
53            Self::C32(values) => values.len(),
54            Self::C64(values) => values.len(),
55        }
56    }
57
58    /// Return whether the payload contains no values.
59    pub fn is_empty(&self) -> bool {
60        self.len() == 0
61    }
62}
63
64/// Error returned while validating, snapshotting, or reconstructing a logical tensor.
65#[derive(Debug, thiserror::Error)]
66pub enum LogicalTensorError {
67    /// The shape product overflowed `usize`.
68    #[error("logical tensor shape product overflows usize: {shape:?}")]
69    ShapeOverflow {
70        /// Rejected tensor shape.
71        shape: Vec<usize>,
72    },
73    /// The payload length does not match the shape.
74    #[error(
75        "logical tensor element count mismatch: shape {shape:?} requires {expected}, got {actual}"
76    )]
77    ElementCountMismatch {
78        /// Tensor shape.
79        shape: Vec<usize>,
80        /// Required element count.
81        expected: usize,
82        /// Supplied element count.
83        actual: usize,
84    },
85    /// Tenferro could not read or construct the host tensor.
86    #[error("logical tensor {operation} failed: {source}")]
87    Tensor {
88        /// Operation that failed.
89        operation: &'static str,
90        /// Original tenferro diagnostic.
91        #[source]
92        source: tenferro_tensor::Error,
93    },
94}
95
96/// Backend-free, dtype-preserving logical host tensor in column-major order.
97///
98/// Use [`CpuExecutionContext::reconstruct`] in the receiving execution domain.
99/// The adapter, not tensor4all, owns serialization and transport.
100///
101/// # Examples
102///
103/// ```
104/// use tensor4all_tensorbackend::{LogicalTensor, LogicalTensorData};
105///
106/// let tensor = LogicalTensor::new(
107///     vec![2, 2],
108///     LogicalTensorData::F64(vec![1.0, 2.0, 3.0, 4.0]),
109/// )?;
110/// assert_eq!(tensor.shape(), &[2, 2]);
111/// assert_eq!(tensor.data().len(), 4);
112/// # Ok::<(), tensor4all_tensorbackend::LogicalTensorError>(())
113/// ```
114#[derive(Clone, Debug, PartialEq)]
115pub struct LogicalTensor {
116    shape: Vec<usize>,
117    data: LogicalTensorData,
118}
119
120impl LogicalTensor {
121    /// Create a validated column-major logical tensor snapshot.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`LogicalTensorError::ShapeOverflow`] when the shape product
126    /// overflows, or [`LogicalTensorError::ElementCountMismatch`] when the data
127    /// length differs from that product.
128    pub fn new(shape: Vec<usize>, data: LogicalTensorData) -> Result<Self, LogicalTensorError> {
129        let expected = shape
130            .iter()
131            .try_fold(1_usize, |count, &dim| count.checked_mul(dim).ok_or(()));
132        let expected = expected.map_err(|()| LogicalTensorError::ShapeOverflow {
133            shape: shape.clone(),
134        })?;
135        if data.len() != expected {
136            return Err(LogicalTensorError::ElementCountMismatch {
137                shape,
138                expected,
139                actual: data.len(),
140            });
141        }
142        Ok(Self { shape, data })
143    }
144
145    /// Snapshot a native host tensor without retaining execution identity.
146    ///
147    /// # Errors
148    ///
149    /// Returns [`LogicalTensorError::Tensor`] if the native tensor is not
150    /// host-readable with its declared dtype.
151    pub fn from_native(tensor: &Tensor) -> Result<Self, LogicalTensorError> {
152        let dtype = tensor.dtype();
153        let data = match dtype {
154            DType::F32 => LogicalTensorData::F32(
155                tensor
156                    .as_slice::<f32>()
157                    .map_err(|source| LogicalTensorError::Tensor {
158                        operation: "snapshot",
159                        source,
160                    })?
161                    .to_vec(),
162            ),
163            DType::F64 => LogicalTensorData::F64(
164                tensor
165                    .as_slice::<f64>()
166                    .map_err(|source| LogicalTensorError::Tensor {
167                        operation: "snapshot",
168                        source,
169                    })?
170                    .to_vec(),
171            ),
172            DType::I32 => LogicalTensorData::I32(
173                tensor
174                    .as_slice::<i32>()
175                    .map_err(|source| LogicalTensorError::Tensor {
176                        operation: "snapshot",
177                        source,
178                    })?
179                    .to_vec(),
180            ),
181            DType::I64 => LogicalTensorData::I64(
182                tensor
183                    .as_slice::<i64>()
184                    .map_err(|source| LogicalTensorError::Tensor {
185                        operation: "snapshot",
186                        source,
187                    })?
188                    .to_vec(),
189            ),
190            DType::Bool => LogicalTensorData::Bool(
191                tensor
192                    .as_slice::<bool>()
193                    .map_err(|source| LogicalTensorError::Tensor {
194                        operation: "snapshot",
195                        source,
196                    })?
197                    .to_vec(),
198            ),
199            DType::C32 => LogicalTensorData::C32(
200                tensor
201                    .as_slice::<Complex32>()
202                    .map_err(|source| LogicalTensorError::Tensor {
203                        operation: "snapshot",
204                        source,
205                    })?
206                    .to_vec(),
207            ),
208            DType::C64 => LogicalTensorData::C64(
209                tensor
210                    .as_slice::<Complex64>()
211                    .map_err(|source| LogicalTensorError::Tensor {
212                        operation: "snapshot",
213                        source,
214                    })?
215                    .to_vec(),
216            ), // An external scalar kind carries a caller-registered Rust type, not a
217            // host payload this crate can snapshot.
218            DType::External(_) => {
219                return Err(LogicalTensorError::Tensor {
220                    operation: "snapshot",
221                    source: tenferro_tensor::Error::unsupported(
222                        "snapshot",
223                        format!("external scalar kind {dtype:?} has no logical-tensor payload"),
224                    ),
225                })
226            }
227        };
228        Self::new(tensor.shape().to_vec(), data)
229    }
230
231    /// Return the logical shape.
232    pub fn shape(&self) -> &[usize] {
233        &self.shape
234    }
235
236    /// Return the scalar dtype.
237    pub fn dtype(&self) -> DType {
238        self.data.dtype()
239    }
240
241    /// Return the owned scalar payload.
242    pub fn data(&self) -> &LogicalTensorData {
243        &self.data
244    }
245
246    fn to_native(&self) -> Result<Tensor, LogicalTensorError> {
247        let result = match &self.data {
248            LogicalTensorData::F32(values) => {
249                Tensor::from_vec_col_major(self.shape.clone(), values.clone())
250            }
251            LogicalTensorData::F64(values) => {
252                Tensor::from_vec_col_major(self.shape.clone(), values.clone())
253            }
254            LogicalTensorData::I32(values) => {
255                Tensor::from_vec_col_major(self.shape.clone(), values.clone())
256            }
257            LogicalTensorData::I64(values) => {
258                Tensor::from_vec_col_major(self.shape.clone(), values.clone())
259            }
260            LogicalTensorData::Bool(values) => {
261                Tensor::from_vec_col_major(self.shape.clone(), values.clone())
262            }
263            LogicalTensorData::C32(values) => {
264                Tensor::from_vec_col_major(self.shape.clone(), values.clone())
265            }
266            LogicalTensorData::C64(values) => {
267                Tensor::from_vec_col_major(self.shape.clone(), values.clone())
268            }
269        };
270        result.map_err(|source| LogicalTensorError::Tensor {
271            operation: "reconstruction",
272            source,
273        })
274    }
275}
276
277impl CpuExecutionContext {
278    /// Reconstruct a logical host tensor in this receiving execution domain.
279    ///
280    /// The returned value carries only host tensor data. Its first plain, graph,
281    /// or eager operation must use this same explicit context.
282    ///
283    /// # Errors
284    ///
285    /// Returns [`LogicalTensorError`] if tenferro rejects the validated shape or
286    /// dtype payload.
287    pub fn reconstruct(&self, tensor: &LogicalTensor) -> Result<Tensor, LogicalTensorError> {
288        let host = tensor.to_native()?;
289        self.with_session(|engine| engine.upload_host_tensor(TensorRead::from_tensor(&host)))
290            .map_err(|source| LogicalTensorError::Tensor {
291                operation: "target-context upload",
292                source: tenferro_tensor::Error::runtime_state_source(
293                    "target-context upload",
294                    source,
295                ),
296            })?
297            .map_err(|source| LogicalTensorError::Tensor {
298                operation: "target-context upload",
299                source,
300            })
301    }
302}
303
304#[cfg(test)]
305mod tests;