use crate::{
DlpackElement,
ffi::{DLDataType, DLDevice, DLDeviceType, DLTensor},
};
use snafu::{Snafu, ensure};
use std::{borrow::Cow, mem, os::raw::c_void};
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("shape pointer is null but ndim is {ndim}"))]
NullShapePtr { ndim: i32 },
#[snafu(display("ndim is negative: {ndim}"))]
NegativeNdim { ndim: i32 },
#[snafu(display("shape dimension {axis} is negative: {value}"))]
NegativeDimension { axis: usize, value: i64 },
#[snafu(display("number of elements overflows usize"))]
NumElementsOverflow,
#[snafu(display("number of bytes overflows usize"))]
NumBytesOverflow,
#[snafu(display("shape length ({shape_len}) does not match strides length ({strides_len})"))]
MismatchedStrides {
shape_len: usize,
strides_len: usize,
},
#[snafu(display("a contiguous Rust slice requires compact row-major strides"))]
NonCompactStrides,
#[snafu(display("tensor must be on CPU to expose a Rust slice, got {device_type:?}"))]
NotCpu { device_type: DLDeviceType },
#[snafu(display("dtype mismatch: expected {expected:?}, got {actual:?}"))]
DtypeMismatch {
expected: DLDataType,
actual: DLDataType,
},
#[snafu(display("tensor is read-only"))]
ReadOnly,
#[snafu(display("tensor data pointer is null for a non-empty tensor"))]
NullData,
#[snafu(display("byte_offset {byte_offset} does not fit in usize"))]
ByteOffsetOverflow { byte_offset: u64 },
#[snafu(display("data pointer plus byte_offset overflows address space"))]
DataPointerOverflow,
#[snafu(display("data pointer {ptr:#x} is not aligned to {align} bytes"))]
MisalignedData { ptr: usize, align: usize },
}
pub fn compact_strides(shape: &[i64]) -> Result<Vec<i64>, Error> {
validate_shape_dimensions(shape)?;
let mut strides = vec![0; shape.len()];
let mut stride = 1i64;
for axis in (0..shape.len()).rev() {
strides[axis] = stride;
stride = stride
.checked_mul(shape[axis])
.ok_or(Error::NumElementsOverflow)?;
}
Ok(strides)
}
pub fn compact_strides_array<T, const N: usize>(shape: [T; N]) -> Result<[i64; N], Error>
where
T: Into<i64> + Copy,
{
let shape = shape.map(Into::into);
validate_shape_dimensions(&shape)?;
let mut strides = [0i64; N];
let mut stride = 1i64;
for axis in (0..N).rev() {
strides[axis] = stride;
stride = stride
.checked_mul(shape[axis])
.ok_or(Error::NumElementsOverflow)?;
}
Ok(strides)
}
pub fn is_compact_strides(shape: &[i64], strides: Option<&[i64]>) -> Result<bool, Error> {
let Some(strides) = strides else {
return Ok(true);
};
ensure!(
shape.len() == strides.len(),
MismatchedStridesSnafu {
shape_len: shape.len(),
strides_len: strides.len()
}
);
Ok(strides == compact_strides(shape)?.as_slice())
}
fn validate_shape_dimensions(shape: &[i64]) -> Result<(), Error> {
for (axis, &value) in shape.iter().enumerate() {
ensure!(value >= 0, NegativeDimensionSnafu { axis, value });
}
Ok(())
}
impl Default for DLTensor {
fn default() -> Self {
Self {
data: std::ptr::null_mut(),
device: DLDevice::default(),
ndim: 0,
dtype: DLDataType::default(),
shape: std::ptr::null_mut(),
strides: std::ptr::null_mut(),
byte_offset: 0,
}
}
}
impl DLTensor {
pub fn shape(&self) -> Result<&[i64], Error> {
ensure!(self.ndim >= 0, NegativeNdimSnafu { ndim: self.ndim });
if self.ndim == 0 {
return Ok(&[]);
}
ensure!(!self.shape.is_null(), NullShapePtrSnafu { ndim: self.ndim });
Ok(unsafe { std::slice::from_raw_parts(self.shape, self.ndim as usize) })
}
pub fn strides(&self) -> Result<Option<&[i64]>, Error> {
ensure!(self.ndim >= 0, NegativeNdimSnafu { ndim: self.ndim });
if self.strides.is_null() || self.ndim == 0 {
return Ok(None);
}
Ok(Some(unsafe {
std::slice::from_raw_parts(self.strides, self.ndim as usize)
}))
}
pub fn strides_or_compact(&self) -> Result<Cow<'_, [i64]>, Error> {
match self.strides()? {
Some(strides) => Ok(Cow::Borrowed(strides)),
None => {
let shape = self.shape()?;
if shape.is_empty() {
Ok(Cow::Borrowed(&[]))
} else {
Ok(Cow::Owned(compact_strides(shape)?))
}
}
}
}
pub fn num_elements(&self) -> Result<usize, Error> {
let shape = self.shape()?;
validate_shape_dimensions(shape)?;
shape.iter().try_fold(1usize, |acc, &dim| {
acc.checked_mul(dim as usize)
.ok_or(Error::NumElementsOverflow)
})
}
pub fn num_bytes(&self) -> Result<usize, Error> {
let bits_per_element = (self.dtype.bits as usize)
.checked_mul(self.dtype.lanes as usize)
.ok_or(Error::NumBytesOverflow)?;
let total_bits = self
.num_elements()?
.checked_mul(bits_per_element)
.ok_or(Error::NumBytesOverflow)?;
Ok(total_bits.div_ceil(8))
}
pub fn is_compact(&self) -> Result<bool, Error> {
is_compact_strides(self.shape()?, self.strides()?)
}
pub fn cpu_data_slice<T: DlpackElement>(&self) -> Result<&[T], Error> {
ensure!(
self.device.device_type == DLDeviceType::CPU,
NotCpuSnafu {
device_type: self.device.device_type
}
);
ensure!(
self.dtype.is::<T>(),
DtypeMismatchSnafu {
expected: T::DTYPE,
actual: self.dtype
}
);
ensure!(self.is_compact()?, NonCompactStridesSnafu);
let num_elements = self.num_elements()?;
if num_elements == 0 {
return Ok(&[]);
}
let data_ptr = self.offset_data_ptr::<T>()?;
Ok(unsafe { std::slice::from_raw_parts(data_ptr, num_elements) })
}
pub fn cpu_data_ptr<T: DlpackElement>(&self) -> Result<*const T, Error> {
ensure!(
self.device.device_type == DLDeviceType::CPU,
NotCpuSnafu {
device_type: self.device.device_type
}
);
ensure!(
self.dtype.is::<T>(),
DtypeMismatchSnafu {
expected: T::DTYPE,
actual: self.dtype
}
);
if self.num_elements()? == 0 {
return Ok(std::ptr::NonNull::<T>::dangling().as_ptr());
}
self.offset_data_ptr::<T>()
}
pub fn cpu_data_ptr_bytes(&self) -> Result<*const u8, Error> {
ensure!(
self.device.device_type == DLDeviceType::CPU,
NotCpuSnafu {
device_type: self.device.device_type
}
);
if self.num_bytes()? == 0 {
return Ok(std::ptr::NonNull::<u8>::dangling().as_ptr());
}
self.offset_data_ptr::<u8>()
}
fn offset_data_ptr<T>(&self) -> Result<*const T, Error> {
ensure!(!self.data.is_null(), NullDataSnafu);
let byte_offset =
usize::try_from(self.byte_offset).map_err(|_| Error::ByteOffsetOverflow {
byte_offset: self.byte_offset,
})?;
let data = self.data.cast::<u8>();
let data_addr = data
.addr()
.checked_add(byte_offset)
.ok_or(Error::DataPointerOverflow)?;
let align = mem::align_of::<T>();
ensure!(
data_addr.is_multiple_of(align),
MisalignedDataSnafu {
ptr: data_addr,
align,
}
);
Ok(data.with_addr(data_addr).cast::<T>())
}
pub fn data_ptr(&self) -> *const c_void {
self.data as *const c_void
}
pub(crate) fn from_parts(shape: *mut i64, strides: *mut i64, ndim: i32) -> Self {
Self {
ndim,
shape,
strides,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strides_or_compact_borrows_explicit_strides() {
let shape = [2i64, 3];
let strides = [10i64, 2];
let tensor = DLTensor {
ndim: 2,
shape: shape.as_ptr() as *mut i64,
strides: strides.as_ptr() as *mut i64,
..DLTensor::default()
};
let actual = tensor.strides_or_compact().unwrap();
assert!(matches!(actual, Cow::Borrowed(_)));
assert_eq!(&*actual, &[10, 2]);
}
#[test]
fn strides_or_compact_computes_implicit_compact_strides() {
let shape = [2i64, 3, 4];
let tensor = DLTensor {
ndim: 3,
shape: shape.as_ptr() as *mut i64,
strides: std::ptr::null_mut(),
..DLTensor::default()
};
let actual = tensor.strides_or_compact().unwrap();
assert!(matches!(actual, Cow::Owned(_)));
assert_eq!(&*actual, &[12, 4, 1]);
}
#[test]
fn strides_or_compact_keeps_scalar_strides_empty() {
let tensor = DLTensor::default();
let actual = tensor.strides_or_compact().unwrap();
assert!(matches!(actual, Cow::Borrowed(_)));
assert!(actual.is_empty());
}
#[test]
fn cpu_data_slice_rejects_non_compact_strides_but_pointer_is_available() {
let data = [1i32, 2, 3, 4];
let shape = [2i64, 2];
let strides = [1i64, 2];
let tensor = DLTensor {
data: data.as_ptr().cast_mut().cast(),
device: DLDevice::CPU,
ndim: 2,
dtype: i32::DTYPE,
shape: shape.as_ptr().cast_mut(),
strides: strides.as_ptr().cast_mut(),
..DLTensor::default()
};
assert!(matches!(
tensor.cpu_data_slice::<i32>(),
Err(Error::NonCompactStrides)
));
assert_eq!(tensor.cpu_data_ptr::<i32>().unwrap(), data.as_ptr());
}
}