use crate::{Completion, Error, Result, Runtime, execution::BufferView};
use std::sync::Arc;
mod linear;
mod ops;
pub use ops::TensorOps;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize)]
pub enum DType {
U8,
I32,
U32,
I64,
F16,
BF16,
F32,
}
impl DType {
pub const fn bytes(self) -> usize {
match self {
Self::U8 => 1,
Self::F16 | Self::BF16 => 2,
Self::I32 | Self::U32 | Self::F32 => 4,
Self::I64 => 8,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize)]
pub enum Layout {
#[default]
General,
Nchw,
Nhwc,
Rows,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize)]
pub struct TensorDesc {
dtype: DType,
shape: Vec<usize>,
strides: Vec<usize>,
layout: Layout,
elements: usize,
bytes: usize,
}
impl TensorDesc {
pub fn new(dtype: DType, shape: impl Into<Vec<usize>>) -> Result<Self> {
let shape = shape.into();
if shape.contains(&0) {
let strides = vec![1; shape.len()];
return Self::strided(dtype, shape, strides);
}
let mut stride = 1usize;
let mut strides = vec![0; shape.len()];
for (index, &dimension) in shape.iter().enumerate().rev() {
strides[index] = stride;
stride = stride
.checked_mul(dimension.max(1))
.ok_or_else(|| Error::Message("tensor stride overflow".into()))?;
}
Self::strided(dtype, shape, strides)
}
pub fn strided(dtype: DType, shape: Vec<usize>, strides: Vec<usize>) -> Result<Self> {
if shape.len() != strides.len() || strides.contains(&0) {
return Err(Error::Message("invalid tensor strides".into()));
}
let empty = shape.contains(&0);
let elements = if empty {
0
} else {
shape
.iter()
.try_fold(1usize, |n, &d| n.checked_mul(d))
.ok_or_else(|| Error::Message("tensor element count overflow".into()))?
};
let mut extent = usize::from(!empty);
if !empty {
let mut axes = shape
.iter()
.copied()
.zip(strides.iter().copied())
.filter(|&(d, _)| d > 1)
.collect::<Vec<_>>();
axes.sort_unstable_by_key(|&(_, stride)| stride);
for (dimension, stride) in axes {
if stride < extent {
return Err(Error::Message("overlapping tensor strides".into()));
}
extent = (dimension - 1)
.checked_mul(stride)
.and_then(|n| n.checked_add(extent))
.ok_or_else(|| Error::Message("tensor extent overflow".into()))?;
}
}
let bytes = extent
.checked_mul(dtype.bytes())
.filter(|&n| n <= isize::MAX as usize)
.ok_or_else(|| Error::Message("tensor byte extent overflow".into()))?;
Ok(Self {
dtype,
shape,
strides,
layout: Layout::General,
elements,
bytes,
})
}
pub fn with_layout(mut self, layout: Layout) -> Result<Self> {
let valid = match layout {
Layout::General => true,
Layout::Rows => self.shape.len() == 2,
Layout::Nchw | Layout::Nhwc => self.shape.len() == 4,
};
if !valid {
return Err(Error::Message("tensor layout rank mismatch".into()));
}
self.layout = layout;
Ok(self)
}
pub fn dtype(&self) -> DType {
self.dtype
}
pub fn shape(&self) -> &[usize] {
&self.shape
}
pub fn strides(&self) -> &[usize] {
&self.strides
}
pub fn layout(&self) -> Layout {
self.layout
}
pub fn elements(&self) -> usize {
self.elements
}
pub fn bytes(&self) -> usize {
self.bytes
}
pub fn is_empty(&self) -> bool {
self.elements == 0
}
pub fn is_contiguous(&self) -> bool {
if self.is_empty() {
return true;
}
let mut stride = 1usize;
for (&dimension, &actual) in self.shape.iter().zip(&self.strides).rev() {
if dimension > 1 && actual != stride {
return false;
}
let Some(next) = stride.checked_mul(dimension.max(1)) else {
return false;
};
stride = next;
}
true
}
}
#[derive(Clone)]
pub struct DeviceTensor {
pub(crate) runtime: Runtime,
pub(crate) desc: TensorDesc,
pub(crate) view: Option<BufferView>,
pub(crate) producer: Completion,
pub(crate) lease: Option<Arc<dyn Send + Sync>>,
}
impl DeviceTensor {
pub fn desc(&self) -> &TensorDesc {
&self.desc
}
pub fn completion(&self) -> &Completion {
&self.producer
}
pub fn binding(&self) -> Option<BufferView> {
self.view.clone().map(|view| match &self.lease {
Some(lease) => view.retain(lease.clone()),
None => view,
})
}
pub fn view(&self, byte_offset: usize, desc: TensorDesc) -> Result<Self> {
let end = byte_offset
.checked_add(desc.bytes())
.filter(|&end| end <= self.desc.bytes())
.ok_or_else(|| Error::Message("tensor view exceeds storage".into()))?;
let origin = self.view.as_ref().map_or(0, BufferView::offset);
if !(origin + byte_offset).is_multiple_of(desc.dtype().bytes()) {
return Err(Error::Message("unaligned tensor view".into()));
}
let view = if desc.is_empty() {
None
} else {
Some(
self.view
.as_ref()
.ok_or_else(|| Error::Message("empty tensor has no storage".into()))?
.slice(byte_offset..end)?,
)
};
Ok(Self {
runtime: self.runtime.clone(),
desc,
view,
producer: self.producer.clone(),
lease: self.lease.clone(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shapes_distinguish_empty_scalar_and_strided_storage() {
let scalar = TensorDesc::new(DType::F32, vec![]).unwrap();
assert_eq!((scalar.elements(), scalar.bytes()), (1, 4));
let empty = TensorDesc::new(DType::F32, vec![2, 0, 3]).unwrap();
assert_eq!((empty.elements(), empty.bytes()), (0, 0));
let huge_empty = TensorDesc::new(DType::F32, vec![usize::MAX, 0, usize::MAX]).unwrap();
assert!(huge_empty.is_empty() && huge_empty.is_contiguous());
let padded = TensorDesc::strided(DType::F32, vec![2, 3], vec![4, 1]).unwrap();
assert_eq!((padded.elements(), padded.bytes()), (6, 28));
assert!(!padded.is_contiguous());
assert!(TensorDesc::strided(DType::F32, vec![2, 3], vec![1, 1]).is_err());
assert!(TensorDesc::new(DType::F32, vec![usize::MAX]).is_err());
assert!(
TensorDesc::new(DType::U8, vec![2, 3])
.unwrap()
.with_layout(Layout::Nchw)
.is_err()
);
let transpose = TensorDesc::strided(DType::U8, vec![3, 2], vec![1, 3]).unwrap();
assert_eq!(transpose.bytes(), 6);
}
}