use bytes::Bytes;
use crate::error::{Error, Result};
use crate::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DType {
F16,
F32,
F64,
I8,
I16,
I32,
I64,
U8,
U16,
U32,
U64,
}
impl DType {
pub const fn size(self) -> usize {
match self {
DType::I8 | DType::U8 => 1,
DType::F16 | DType::I16 | DType::U16 => 2,
DType::F32 | DType::I32 | DType::U32 => 4,
DType::F64 | DType::I64 | DType::U64 => 8,
}
}
pub const fn long_name(self) -> &'static str {
match self {
DType::F16 => "float16",
DType::F32 => "float32",
DType::F64 => "float64",
DType::I8 => "int8",
DType::I16 => "int16",
DType::I32 => "int32",
DType::I64 => "int64",
DType::U8 => "uint8",
DType::U16 => "uint16",
DType::U32 => "uint32",
DType::U64 => "uint64",
}
}
pub const fn short_name(self) -> &'static str {
match self {
DType::F16 => "f2",
DType::F32 => "f4",
DType::F64 => "f8",
DType::I8 => "i1",
DType::I16 => "i2",
DType::I32 => "i4",
DType::I64 => "i8",
DType::U8 => "u1",
DType::U16 => "u2",
DType::U32 => "u4",
DType::U64 => "u8",
}
}
pub fn parse(name: &str) -> Result<DType> {
let dt = match name {
"float16" | "f2" => DType::F16,
"float32" | "f4" => DType::F32,
"float64" | "f8" => DType::F64,
"int8" | "i1" => DType::I8,
"int16" | "i2" => DType::I16,
"int32" | "i4" => DType::I32,
"int64" | "i8" => DType::I64,
"uint8" | "u1" => DType::U8,
"uint16" | "u2" => DType::U16,
"uint32" | "u4" => DType::U32,
"uint64" | "u8" => DType::U64,
other => return Err(Error::unsupported(format!("dtype {other}"))),
};
Ok(dt)
}
pub const fn is_float(self) -> bool {
matches!(self, DType::F16 | DType::F32 | DType::F64)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tensor {
dtype: DType,
shape: Vec<usize>,
data: Bytes,
}
impl Tensor {
pub fn new(dtype: DType, shape: Vec<usize>, data: impl Into<Bytes>) -> Result<Tensor> {
let data = data.into();
let expected = shape.iter().product::<usize>() * dtype.size();
if data.len() != expected {
return Err(Error::format(format!(
"tensor data is {} bytes but shape {shape:?} of {} needs {expected}",
data.len(),
dtype.long_name()
)));
}
Ok(Tensor { dtype, shape, data })
}
pub fn from_f32(values: &[f32]) -> Tensor {
Self::from_f32_shaped(values, vec![values.len()])
}
pub fn from_f32_shaped(values: &[f32], shape: Vec<usize>) -> Tensor {
assert_eq!(shape.iter().product::<usize>(), values.len(), "shape does not match value count");
let mut data = Vec::with_capacity(values.len() * 4);
for v in values {
data.extend_from_slice(&v.to_ne_bytes());
}
Tensor { dtype: DType::F32, shape, data: data.into() }
}
pub fn from_u8_shaped(values: Vec<u8>, shape: Vec<usize>) -> Tensor {
assert_eq!(shape.iter().product::<usize>(), values.len(), "shape does not match value count");
Tensor { dtype: DType::U8, shape, data: values.into() }
}
pub fn dtype(&self) -> DType {
self.dtype
}
pub fn shape(&self) -> &[usize] {
&self.shape
}
pub fn ndim(&self) -> usize {
self.shape.len()
}
pub fn numel(&self) -> usize {
self.shape.iter().product()
}
pub fn data(&self) -> &Bytes {
&self.data
}
pub fn into_data(self) -> Bytes {
self.data
}
pub fn reshape(&self, shape: Vec<usize>) -> Result<Tensor> {
Tensor::new(self.dtype, shape, self.data.clone())
}
pub fn to_f64_vec(&self) -> Vec<f64> {
let n = self.numel();
let mut out = Vec::with_capacity(n);
let d = &self.data[..];
for i in 0..n {
out.push(self.element_at(d, i));
}
out
}
pub fn to_f32_vec(&self) -> Vec<f32> {
self.to_f64_vec().into_iter().map(|v| v as f32).collect()
}
pub fn get(&self, index: usize) -> f64 {
assert!(index < self.numel(), "index {index} out of bounds");
self.element_at(&self.data[..], index)
}
fn element_at(&self, d: &[u8], i: usize) -> f64 {
let s = self.dtype.size();
let b = &d[i * s..(i + 1) * s];
match self.dtype {
DType::U8 => b[0] as f64,
DType::I8 => b[0] as i8 as f64,
DType::U16 => u16::from_ne_bytes([b[0], b[1]]) as f64,
DType::I16 => i16::from_ne_bytes([b[0], b[1]]) as f64,
DType::F16 => f16_to_f64(u16::from_ne_bytes([b[0], b[1]])),
DType::U32 => u32::from_ne_bytes([b[0], b[1], b[2], b[3]]) as f64,
DType::I32 => i32::from_ne_bytes([b[0], b[1], b[2], b[3]]) as f64,
DType::F32 => f32::from_ne_bytes([b[0], b[1], b[2], b[3]]) as f64,
DType::U64 => u64::from_ne_bytes(b.try_into().expect("8 bytes")) as f64,
DType::I64 => i64::from_ne_bytes(b.try_into().expect("8 bytes")) as f64,
DType::F64 => f64::from_ne_bytes(b.try_into().expect("8 bytes")),
}
}
}
fn f16_to_f64(bits: u16) -> f64 {
let sign = if bits & 0x8000 != 0 { -1.0f64 } else { 1.0 };
let exponent = ((bits >> 10) & 0x1f) as i32;
let mantissa = (bits & 0x3ff) as f64;
match exponent {
0 => sign * mantissa * exp2(-24),
0x1f if mantissa == 0.0 => sign * f64::INFINITY,
0x1f => f64::NAN,
_ => sign * (1.0 + mantissa / 1024.0) * exp2(exponent - 15),
}
}
fn exp2(n: i32) -> f64 {
let mut out = 1.0f64;
for _ in 0..n.abs() {
if n >= 0 {
out *= 2.0;
} else {
out /= 2.0;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_f32_values() {
let t = Tensor::from_f32_shaped(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]);
assert_eq!(t.shape(), &[2, 3]);
assert_eq!(t.numel(), 6);
assert_eq!(t.dtype(), DType::F32);
assert_eq!(t.to_f64_vec(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
assert_eq!(t.get(4), 5.0);
}
#[test]
fn rejects_mismatched_shapes() {
let err = Tensor::new(DType::F32, vec![3], vec![0u8; 8]);
assert!(err.is_err());
}
#[test]
fn reshapes_without_copying_semantics() {
let t = Tensor::from_f32(&[1.0, 2.0, 3.0, 4.0]);
let r = t.reshape(vec![2, 2]).unwrap();
assert_eq!(r.shape(), &[2, 2]);
assert!(t.reshape(vec![3, 3]).is_err());
}
#[test]
fn parses_dtype_names() {
assert_eq!(DType::parse("float32").unwrap(), DType::F32);
assert_eq!(DType::parse("f4").unwrap(), DType::F32);
assert_eq!(DType::parse("u1").unwrap(), DType::U8);
assert!(DType::parse("complex64").is_err());
}
}