use std::collections::{HashMap, HashSet};
use std::fmt;
use std::hash::{BuildHasherDefault, Hasher};
use std::panic::Location;
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
use std::thread::ThreadId;
use smallvec::SmallVec;
use crate::buffer::Buffer;
use crate::primitive::Primitive;
use crate::shape::{self, Flags, Shape, Strides};
use crate::transforms::TraceId;
use crate::{DType, Device, Element, Error, Result, Stream, default_stream};
#[derive(Clone)]
#[must_use = "arrays are lazy: an unused result computes nothing (and may hold an error)"]
pub struct Array {
pub(crate) node: Arc<Node>,
pub(crate) output: u32,
}
pub(crate) struct Node {
pub(crate) stream: Stream,
pub(crate) trace: Option<TraceId>,
pub(crate) outputs: SmallVec<[Output; 1]>,
pub(crate) state: Mutex<State>,
pub(crate) failed: bool,
}
pub(crate) struct Output {
pub(crate) dtype: DType,
pub(crate) shape: Shape,
pub(crate) ishape: SmallVec<[i32; 4]>,
pub(crate) strides: Strides,
pub(crate) flags: Flags,
pub(crate) size: usize,
pub(crate) slot: OnceLock<Result<Data>>,
pub(crate) inflight: Arc<InFlight>,
}
#[derive(Default)]
pub(crate) struct InFlight {
state: Mutex<InFlightState>,
chunk: OnceLock<ChunkStatus>,
}
pub(crate) type ChunkStatus = Arc<OnceLock<Error>>;
#[derive(Default)]
pub(crate) enum InFlightState {
#[default]
Unset,
#[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
Set(Data),
Donated,
}
impl InFlight {
pub(crate) fn lock(&self) -> MutexGuard<'_, InFlightState> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
#[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
pub(crate) fn set_from(&self, data: Data, chunk: &ChunkStatus) {
let _ = self.chunk.set(chunk.clone());
self.set(data);
}
#[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
pub(crate) fn chunk(&self) -> Option<ChunkStatus> {
self.chunk.get().cloned()
}
#[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
pub(crate) fn set(&self, data: Data) {
let mut state = self.lock();
if matches!(*state, InFlightState::Unset) {
*state = InFlightState::Set(data);
}
}
pub(crate) fn get(&self) -> Option<Data> {
match &*self.lock() {
InFlightState::Set(data) => Some(data.clone()),
_ => None,
}
}
}
#[derive(Clone)]
pub(crate) struct Data {
pub(crate) buffer: Arc<Buffer>,
pub(crate) offset: isize,
}
impl Data {
pub(crate) fn new(buffer: Buffer) -> Self {
Self {
buffer: Arc::new(buffer),
offset: 0,
}
}
}
pub(crate) enum State {
Pending {
primitive: Primitive,
inputs: Vec<Array>,
},
Running {
owner: ThreadId,
done: Arc<Completion>,
},
Retained {
primitive: Primitive,
inputs: Vec<Array>,
},
Done,
Failed {
error: Error,
},
}
#[derive(Default)]
pub(crate) struct Completion {
done: Mutex<bool>,
cv: Condvar,
}
impl Completion {
pub(crate) fn finish(&self) {
*self.done.lock().unwrap_or_else(PoisonError::into_inner) = true;
self.cv.notify_all();
}
pub(crate) fn wait(&self) {
let mut done = self.done.lock().unwrap_or_else(PoisonError::into_inner);
while !*done {
done = self.cv.wait(done).unwrap_or_else(PoisonError::into_inner);
}
}
}
#[derive(Default, Clone, Copy)]
pub(crate) struct NodeHasher(u64);
impl Hasher for NodeHasher {
fn finish(&self) -> u64 {
self.0.rotate_left(26)
}
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.write_u64(u64::from(b));
}
}
fn write_u64(&mut self, n: u64) {
self.0 = (self.0.rotate_left(5) ^ n).wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
fn write_u32(&mut self, n: u32) {
self.write_u64(u64::from(n));
}
fn write_usize(&mut self, n: usize) {
self.write_u64(n as u64);
}
}
pub(crate) type NodeSet = HashSet<*const Node, BuildHasherDefault<NodeHasher>>;
pub(crate) type NodeMap<K, V> = HashMap<K, V, BuildHasherDefault<NodeHasher>>;
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Array>();
};
impl Node {
pub(crate) fn lock(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn is_tracer(&self) -> bool {
self.trace.is_some_and(crate::transforms::is_live)
}
}
impl Output {
#[cold]
fn invalid(shape: &[usize], dtype: DType) -> Error {
if shape.iter().any(|&d| i32::try_from(d).is_err()) {
Error::invalid_shape("shape", shape.iter().map(|&d| d as i64))
} else {
shape::checked_nbytes(shape, dtype.size())
.err()
.unwrap_or_else(|| Error::invalid_shape("shape", shape.iter().map(|&d| d as i64)))
}
}
pub(crate) fn new(
dtype: DType,
shape: Shape,
strides: Option<Strides>,
slot: OnceLock<Result<Data>>,
) -> Result<Self> {
let mut size = 1usize;
let mut ishape = SmallVec::<[i32; 4]>::new();
for &d in &shape {
let (Some(n), Ok(di)) = (size.checked_mul(d), i32::try_from(d)) else {
return Err(Self::invalid(&shape, dtype));
};
size = n;
ishape.push(di);
}
if size
.checked_mul(dtype.size())
.is_none_or(|n| n > isize::MAX as usize)
{
return Err(Self::invalid(&shape, dtype));
}
let strides = strides.unwrap_or_else(|| shape::row_contiguous_strides(&shape));
let flags = shape::flags_for(&shape, &strides);
Ok(Self {
dtype,
shape,
ishape,
strides,
flags,
size,
slot,
inflight: Arc::default(),
})
}
}
impl Array {
pub(crate) fn from_buffer(buffer: Buffer, dtype: DType, shape: &[usize]) -> Result<Self> {
Self::leaf(Ok(Data::new(buffer)), dtype, shape.into())
}
pub(crate) fn leaf(data: Result<Data>, dtype: DType, shape: Shape) -> Result<Self> {
let output = Output::new(dtype, shape, None, OnceLock::from(data))?;
Ok(Self::from_node(Node {
stream: default_stream(),
trace: None,
outputs: SmallVec::from_buf([output]),
state: Mutex::new(State::Done),
failed: false,
}))
}
#[cold]
pub(crate) fn failed(error: Error, dtype: DType, shape: &[usize]) -> Self {
let output = Output::new(
dtype,
shape.into(),
None,
OnceLock::from(Err(error.clone())),
)
.unwrap_or_else(|_| {
Output::new(dtype, Shape::new(), None, OnceLock::from(Err(error)))
.unwrap_or_else(|_| unreachable!("a 0-D output is always valid"))
});
Self::from_node(Node {
stream: default_stream(),
trace: None,
outputs: SmallVec::from_buf([output]),
state: Mutex::new(State::Done),
failed: true,
})
}
#[inline]
pub(crate) fn sticky(
op: &'static str,
at: &'static Location<'static>,
inputs: &[&Array],
f: impl FnOnce() -> Result<Array>,
) -> Array {
match f() {
Ok(out) => out,
Err(error) => Self::failed_like(Self::sticky_error(op, at, inputs, error), inputs),
}
}
#[inline]
pub(crate) fn sticky_n<const N: usize>(
op: &'static str,
at: &'static Location<'static>,
inputs: &[&Array],
f: impl FnOnce() -> Result<[Array; N]>,
) -> [Array; N] {
match f() {
Ok(out) => out,
Err(error) => {
let error = Self::sticky_error(op, at, inputs, error);
std::array::from_fn(|_| Self::failed_like(error.clone(), inputs))
}
}
}
#[inline]
pub(crate) fn sticky_vec(
op: &'static str,
at: &'static Location<'static>,
inputs: &[&Array],
count: usize,
f: impl FnOnce() -> Result<Vec<Array>>,
) -> Vec<Array> {
match f() {
Ok(out) => out,
Err(error) => {
let error = Self::sticky_error(op, at, inputs, error);
(0..count)
.map(|_| Self::failed_like(error.clone(), inputs))
.collect()
}
}
}
#[cold]
fn sticky_error(
op: &'static str,
at: &'static Location<'static>,
inputs: &[&Array],
error: Error,
) -> Error {
inputs
.iter()
.find_map(|a| a.slot_error())
.unwrap_or_else(|| error.locate(op, at))
}
#[cold]
pub(crate) fn failed_like(error: Error, inputs: &[&Array]) -> Self {
match inputs.first() {
Some(a) => Self::failed(error, a.dtype(), a.dims()),
None => Self::failed(error, DType::F32, &[]),
}
}
pub(crate) fn from_op(primitive: Primitive, inputs: Vec<Array>) -> Result<Vec<Array>> {
let trace = crate::transforms::inherited(&inputs);
Self::record(primitive, inputs, trace)
}
pub(crate) fn record(
primitive: Primitive,
inputs: Vec<Array>,
trace: Option<TraceId>,
) -> Result<Vec<Array>> {
if let Some(err) = inputs.iter().find_map(Array::slot_error) {
return Self::record_failed(&primitive, &inputs, err);
}
Self::record_unchecked(primitive, inputs, trace)
}
#[cold]
fn record_failed(primitive: &Primitive, inputs: &[Array], err: Error) -> Result<Vec<Array>> {
let specs = primitive.output_specs(inputs).map_err(|_| err.clone())?;
Ok(specs
.into_iter()
.map(|spec| Self::failed(err.clone(), spec.dtype, &spec.shape))
.collect())
}
pub(crate) fn record_unchecked(
primitive: Primitive,
inputs: Vec<Array>,
trace: Option<TraceId>,
) -> Result<Vec<Array>> {
let outputs = primitive
.output_specs(&inputs)?
.into_iter()
.map(|spec| Output::new(spec.dtype, spec.shape, spec.strides, OnceLock::new()))
.collect::<Result<SmallVec<_>>>()?;
let count = outputs.len() as u32;
let node = Arc::new(Node {
stream: default_stream(),
trace,
outputs,
state: Mutex::new(State::Pending { primitive, inputs }),
failed: false,
});
Ok((0..count)
.map(|output| Self {
node: node.clone(),
output,
})
.collect())
}
fn from_node(node: Node) -> Self {
Self {
node: Arc::new(node),
output: 0,
}
}
pub(crate) fn placeholder(dtype: DType, shape: &[usize]) -> Result<Array> {
let failed = Self::failed(
Error::InvalidArgument(
"arrays cannot be evaluated while a function is traced (compile)".into(),
),
dtype,
&[],
);
let primitive = Primitive::Broadcast(shape.to_vec());
let trace = Some(crate::transforms::PLACEHOLDER);
Ok(Self::record_unchecked(primitive, vec![failed], trace)?.swap_remove(0))
}
pub(crate) fn out(&self) -> &Output {
&self.node.outputs[self.output as usize]
}
pub(crate) fn slot_error(&self) -> Option<Error> {
match self.out().slot.get() {
Some(Err(e)) => Some(e.clone()),
_ => None,
}
}
pub fn is_failed(&self) -> bool {
self.node.failed
}
#[track_caller]
pub fn failure(op: &'static str, kind: crate::OpErrorKind) -> Array {
Self::failed(
Error::op(op, kind).locate(op, Location::caller()),
DType::F32,
&[],
)
}
pub fn error(&self) -> Option<&Error> {
match (self.node.failed, self.out().slot.get()) {
(true, Some(Err(e))) => Some(e),
_ => None,
}
}
pub fn check(self) -> Result<Array> {
if self.node.failed
&& let Some(error) = self.slot_error()
{
return Err(error);
}
Ok(self)
}
pub fn shape(&self) -> &[i32] {
&self.out().ishape
}
#[track_caller]
pub fn shape_n<const N: usize>(&self) -> Result<[i32; N]> {
if let Some(e) = self.error() {
return Err(e.clone());
}
<[i32; N]>::try_from(self.shape()).map_err(|_| {
Error::InvalidArgument(format!(
"shape_n: expected {N} dimensions, the array has shape {:?}",
self.shape()
))
.locate("shape_n", Location::caller())
})
}
pub(crate) fn dims(&self) -> &[usize] {
&self.out().shape
}
pub fn ndim(&self) -> usize {
self.out().shape.len()
}
pub fn size(&self) -> usize {
self.out().size
}
pub fn dtype(&self) -> DType {
self.out().dtype
}
pub fn itemsize(&self) -> usize {
self.dtype().size()
}
pub fn nbytes(&self) -> usize {
self.size() * self.itemsize()
}
pub(crate) fn flags(&self) -> Flags {
self.out().flags
}
pub(crate) fn strides(&self) -> &[isize] {
&self.out().strides
}
pub fn stream(&self) -> Stream {
self.node.stream
}
pub fn device(&self) -> Device {
self.node.stream.device
}
pub fn is_evaluated(&self) -> bool {
self.out().slot.get().is_some()
}
pub fn eval(&self) -> Result<()> {
crate::eval(std::slice::from_ref(self))
}
pub fn to_vec<T: Element>(&self) -> Result<Vec<T>> {
if let Some(e) = self.error() {
return Err(e.clone());
}
if T::DTYPE != self.dtype() {
return Err(Error::dtype_mismatch("to_vec", self.dtype(), T::DTYPE));
}
self.eval()?;
let data = self.data().expect("eval succeeded, so data is set");
let out = self.out();
if out.size == 0 {
Ok(Vec::new())
} else if out.flags.row_contiguous || out.size == 1 {
let start = data.offset.max(0) as usize * out.dtype.size();
Ok(T::read_bytes(
&data.buffer.bytes()[start..start + self.nbytes()],
))
} else {
let gathered = crate::kernels::contiguous(out, data)?;
Ok(T::read_bytes(gathered.bytes()))
}
}
pub fn item<T: Element>(&self) -> Result<T> {
if let Some(e) = self.error() {
return Err(e.clone());
}
if self.size() != 1 {
return Err(Error::InvalidArgument(format!(
"item() needs exactly one element, the array has shape {:?}",
self.dims()
)));
}
if T::DTYPE != self.dtype() {
return Err(Error::dtype_mismatch("item", self.dtype(), T::DTYPE));
}
Ok(self.to_vec::<T>()?[0])
}
pub fn buffer_id(&self) -> Option<usize> {
self.data().map(|d| Arc::as_ptr(&d.buffer) as usize)
}
pub(crate) fn data(&self) -> Option<&Data> {
self.out().slot.get().and_then(|r| r.as_ref().ok())
}
}
impl fmt::Debug for Array {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut d = f.debug_struct("Array");
d.field("shape", &self.shape())
.field("dtype", &self.dtype())
.field("device", &self.device());
match self.out().slot.get() {
None => d.field("status", &"unevaluated"),
Some(Ok(_)) => match crate::display::values(self) {
Ok(values) => d.field("values", &format_args!("{values}")),
Err(_) => d.field("status", &"evaluated"),
},
Some(Err(e)) if self.node.failed => d.field("failed", &format_args!("{e}")),
Some(Err(e)) => d.field("error", &format_args!("{e}")),
};
d.finish()
}
}
impl fmt::Display for Array {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match crate::display::values(self) {
Ok(values) => write!(f, "array({values}, dtype={})", self.dtype()),
Err(e) => write!(f, "<failed: {e}>"),
}
}
}
impl Default for Array {
fn default() -> Self {
crate::creation::from_data(&[0.0f32], &[])
.unwrap_or_else(|e| Self::failed(e, DType::F32, &[]))
}
}
impl Drop for Node {
fn drop(&mut self) {
let mut stack = take_inputs(self);
while let Some(Array { node, .. }) = stack.pop() {
if let Some(mut node) = Arc::into_inner(node) {
stack.append(&mut take_inputs(&mut node));
}
}
}
}
fn take_inputs(node: &mut Node) -> Vec<Array> {
let state = node.state.get_mut().unwrap_or_else(PoisonError::into_inner);
match std::mem::replace(state, State::Done) {
State::Pending { inputs, .. } | State::Retained { inputs, .. } => inputs,
State::Running { .. } | State::Done | State::Failed { .. } => Vec::new(),
}
}
#[cfg(test)]
impl Array {
pub(crate) fn primitive_t(&self) -> Option<Primitive> {
crate::graph::primitive(self)
}
pub(crate) fn inputs_t(&self) -> Vec<Array> {
crate::graph::inputs(self)
}
pub(crate) fn id_t(&self) -> usize {
crate::graph::id(self)
}
pub(crate) fn siblings_t(&self) -> Vec<Array> {
crate::graph::siblings(self)
}
pub(crate) fn output_index_t(&self) -> usize {
crate::graph::output_index(self)
}
pub(crate) fn is_tracer_t(&self) -> bool {
crate::graph::is_tracer(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::OpErrorKind;
#[test]
fn graph_accessors_describe_pending_nodes() {
let a = Array::from_slice(&[1i32, 2], [2]);
let b = Array::from_slice(&[5i32, 7], [2]);
let outs =
crate::graph::from_primitive(Primitive::AddMultiply, vec![a.clone(), b]).unwrap();
let (sum, product) = (&outs[0], &outs[1]);
assert_eq!(sum.id_t(), product.id_t(), "siblings share their operation");
assert_ne!(sum.id_t(), a.id_t());
assert_eq!((sum.output_index_t(), product.output_index_t()), (0, 1));
assert_eq!(product.siblings_t().len(), 2);
assert_eq!(sum.inputs_t()[0].id_t(), a.id_t());
assert!(a.inputs_t().is_empty(), "host data has no inputs");
assert!(!sum.is_tracer_t());
product.eval().unwrap();
assert!(sum.inputs_t().is_empty(), "evaluated: detached");
let p = Array::placeholder(DType::F32, &[3]).unwrap();
let q = &p + 1.0f32;
assert!(p.is_tracer_t() && q.is_tracer_t());
assert_eq!(q.dims(), [3]);
assert!(q.eval().is_err(), "placeholders cannot be evaluated");
}
#[test]
fn failed_leaves_are_sticky_and_typed() {
let error = Error::size_mismatch("from_slice", 6, 5);
let f = Array::failed(error.clone(), DType::F16, &[2, 3]);
assert!(f.is_failed());
assert_eq!(f.shape(), [2, 3]);
assert_eq!(f.dtype(), DType::F16);
assert_eq!(f.eval(), Err(error.clone()));
assert_eq!(f.clone().check().unwrap_err(), error);
let g = crate::ops::imp::reshape(&f, &[3, 2]).unwrap();
assert!(g.is_failed() && g.primitive_t().is_none());
assert_eq!(g.shape(), [3, 2]);
assert_eq!(g.eval(), Err(error));
let ok = &Array::from(1.0f32) + 1.0;
assert!(!ok.is_failed());
let ok = ok.check().unwrap();
assert!(!ok.is_evaluated());
}
#[test]
fn shapes_are_i32_and_bounded() {
let a = crate::ops::imp::zeros(&[2, 3, 4], DType::U8).unwrap();
let shape: &[i32] = a.shape();
assert_eq!(shape, [2, 3, 4]);
assert_eq!(a.shape_n::<3>().unwrap(), [2, 3, 4]);
assert!(a.shape_n::<2>().is_err(), "rank mismatch");
let big = crate::ops::imp::broadcast_to(&Array::from(0u8), &[1usize << 31]).unwrap_err();
assert!(
matches!(big.kind(), Some(OpErrorKind::InvalidShape(_))),
"{big}"
);
let fits = crate::ops::imp::broadcast_to(&Array::from(0u8), &[i32::MAX as usize]).unwrap();
assert_eq!(fits.shape(), [i32::MAX]);
}
#[test]
fn node_hasher_spreads_aligned_addresses() {
use std::hash::BuildHasher;
let build = BuildHasherDefault::<NodeHasher>::default();
let buckets: HashSet<u64> = (0..1024u64)
.map(|i| {
build.hash_one(std::ptr::without_provenance::<Node>(
0x6000_0000_0000 + i as usize * 336,
)) & 1023
})
.collect();
assert!(
buckets.len() > 600,
"only {} of 1024 buckets used",
buckets.len()
);
}
#[test]
fn metadata_without_eval() {
let a = Array::from_slice(&[1i32, 2, 3, 4, 5, 6], [2, 3]);
let c = crate::ops::imp::add(&a, &a).unwrap();
assert!(!c.is_evaluated());
assert_eq!(c.dims(), &[2, 3]);
assert_eq!(c.ndim(), 2);
assert_eq!(c.size(), 6);
assert_eq!(c.dtype(), DType::I32);
assert_eq!(c.itemsize(), 4);
assert_eq!(c.nbytes(), 24);
assert_eq!(c.strides(), &[3, 1]);
assert!(c.flags().row_contiguous);
assert_eq!(c.device(), crate::default_device());
assert_eq!(
c.primitive_t(),
Some(Primitive::Binary(crate::graph::BinaryOp::Add))
);
assert!(!c.is_evaluated(), "metadata queries must not evaluate");
}
#[test]
fn item_on_scalar_and_non_scalar() {
assert_eq!(Array::from(7u16).item::<u16>().unwrap(), 7);
let a = Array::from_slice(&[1.0f32, 2.0], [2]);
assert!(matches!(a.item::<f32>(), Err(Error::InvalidArgument(_))));
assert!(matches!(
Array::from(1.0f32).item::<f64>(),
Err(Error::Op {
kind: OpErrorKind::DTypeMismatch { .. },
..
})
));
}
#[test]
fn debug_does_not_evaluate() {
let a = Array::from(1i8);
let c = crate::ops::imp::add(&a, &a).unwrap();
assert!(format!("{c:?}").contains("unevaluated"));
assert!(!c.is_evaluated());
}
}