Skip to main content

nuvec_core/
array.rs

1//! The lazy array handle and the graph nodes behind it.
2
3use std::fmt;
4use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
5
6use smallvec::SmallVec;
7
8use crate::buffer::Buffer;
9use crate::primitive::Primitive;
10use crate::shape::{self, Flags, Shape, Strides};
11use crate::{DType, Device, Element, Error, Result, Stream, default_stream};
12
13/// A lazily evaluated n-dimensional array.
14///
15/// Cloning is cheap: clones share the computation and its result. Operations record a
16/// graph node and return immediately; values are computed on [`Array::eval`],
17/// [`crate::eval`], or when read back.
18#[derive(Clone)]
19pub struct Array {
20    pub(crate) node: Arc<Node>,
21    pub(crate) output: u32,
22}
23
24/// One recorded computation, possibly with several outputs.
25pub(crate) struct Node {
26    pub(crate) stream: Stream,
27    pub(crate) outputs: SmallVec<[Output; 1]>,
28    pub(crate) state: Mutex<State>,
29}
30
31/// Metadata and (once evaluated) data of one node output.
32pub(crate) struct Output {
33    pub(crate) dtype: DType,
34    pub(crate) shape: Shape,
35    pub(crate) strides: Strides,
36    pub(crate) flags: Flags,
37    pub(crate) size: usize,
38    /// Set exactly once, when the node is evaluated (or at creation for leaves).
39    pub(crate) slot: OnceLock<Result<Data>>,
40}
41
42/// Evaluated data: elements of a (possibly shared) buffer, starting at `offset` and laid
43/// out by the output's strides. Views share their input's buffer.
44#[derive(Clone)]
45pub(crate) struct Data {
46    pub(crate) buffer: Arc<Buffer>,
47    /// Position of the first element, in elements.
48    pub(crate) offset: isize,
49}
50
51impl Data {
52    pub(crate) fn new(buffer: Buffer) -> Self {
53        Self {
54            buffer: Arc::new(buffer),
55            offset: 0,
56        }
57    }
58}
59
60/// Whether a node still has to run. `Done` nodes have dropped their inputs (detach).
61pub(crate) enum State {
62    Pending {
63        primitive: Primitive,
64        inputs: Vec<Array>,
65    },
66    Done,
67}
68
69const _: () = {
70    const fn assert_send_sync<T: Send + Sync>() {}
71    assert_send_sync::<Array>();
72};
73
74impl Node {
75    pub(crate) fn lock(&self) -> MutexGuard<'_, State> {
76        self.state.lock().unwrap_or_else(PoisonError::into_inner)
77    }
78}
79
80impl Output {
81    pub(crate) fn new(
82        dtype: DType,
83        shape: Shape,
84        strides: Option<Strides>,
85        slot: OnceLock<Result<Data>>,
86    ) -> Result<Self> {
87        shape::checked_nbytes(&shape, dtype.size())?;
88        let size = shape::checked_size(&shape)?;
89        let strides = strides.unwrap_or_else(|| shape::row_contiguous_strides(&shape));
90        let flags = shape::flags_for(&shape, &strides);
91        Ok(Self {
92            dtype,
93            shape,
94            strides,
95            flags,
96            size,
97            slot,
98        })
99    }
100}
101
102impl Array {
103    // ── Construction ─────────────────────────────────────────────────────────
104
105    /// An array holding `data` with the given `shape`.
106    ///
107    /// Fails with [`Error::SizeMismatch`] if `data.len()` is not the shape's element count.
108    pub fn from_slice<T: Element>(data: &[T], shape: &[usize]) -> Result<Self> {
109        let expected = shape::checked_size(shape)?;
110        if data.len() != expected {
111            return Err(Error::SizeMismatch {
112                expected,
113                actual: data.len(),
114            });
115        }
116        let mut buffer = Buffer::try_alloc(shape::checked_nbytes(shape, T::DTYPE.size())?)?;
117        T::write_bytes(data, buffer.bytes_mut());
118        Self::leaf(Ok(Data::new(buffer)), T::DTYPE, shape.into())
119    }
120
121    /// An array of the given `shape` with every element set to `value`.
122    pub fn full<T: Element>(shape: &[usize], value: T) -> Result<Self> {
123        let itemsize = T::DTYPE.size();
124        let mut buffer = Buffer::try_alloc(shape::checked_nbytes(shape, itemsize)?)?;
125        let mut element = vec![0u8; itemsize];
126        T::write_bytes(&[value], &mut element);
127        for chunk in buffer.bytes_mut().chunks_exact_mut(itemsize) {
128            chunk.copy_from_slice(&element);
129        }
130        Self::leaf(Ok(Data::new(buffer)), T::DTYPE, shape.into())
131    }
132
133    /// A zero-dimensional array holding `value`.
134    pub fn scalar<T: Element>(value: T) -> Self {
135        Self::from_slice(&[value], &[]).expect("a scalar always matches its shape")
136    }
137
138    /// An evaluated array over a row-contiguous `buffer`.
139    pub(crate) fn from_buffer(buffer: Buffer, dtype: DType, shape: &[usize]) -> Result<Self> {
140        Self::leaf(Ok(Data::new(buffer)), dtype, shape.into())
141    }
142
143    /// A node that is already evaluated, holding `data` (or a sticky error).
144    pub(crate) fn leaf(data: Result<Data>, dtype: DType, shape: Shape) -> Result<Self> {
145        let output = Output::new(dtype, shape, None, OnceLock::from(data))?;
146        Ok(Self::from_node(Node {
147            stream: default_stream(),
148            outputs: SmallVec::from_buf([output]),
149            state: Mutex::new(State::Done),
150        }))
151    }
152
153    /// An array whose evaluation returns `error`. Used where an error cannot be returned
154    /// directly, such as from operators.
155    pub(crate) fn failed(error: Error, dtype: DType, shape: &[usize]) -> Self {
156        Self::leaf(Err(error.clone()), dtype, shape.into())
157            .unwrap_or_else(|_| Self::leaf(Err(error), dtype, Shape::new()).expect("scalar shape"))
158    }
159
160    /// Records `primitive` applied to `inputs` and returns one array per output.
161    ///
162    /// Inputs that already failed short-circuit with their error.
163    pub(crate) fn from_op(primitive: Primitive, inputs: Vec<Array>) -> Result<Vec<Array>> {
164        if let Some(err) = inputs.iter().find_map(Array::error) {
165            return Err(err);
166        }
167        let outputs = primitive
168            .output_specs(&inputs)?
169            .into_iter()
170            .map(|spec| Output::new(spec.dtype, spec.shape, spec.strides, OnceLock::new()))
171            .collect::<Result<SmallVec<_>>>()?;
172        let count = outputs.len() as u32;
173        let node = Arc::new(Node {
174            stream: default_stream(),
175            outputs,
176            state: Mutex::new(State::Pending { primitive, inputs }),
177        });
178        Ok((0..count)
179            .map(|output| Self {
180                node: node.clone(),
181                output,
182            })
183            .collect())
184    }
185
186    fn from_node(node: Node) -> Self {
187        Self {
188            node: Arc::new(node),
189            output: 0,
190        }
191    }
192
193    pub(crate) fn out(&self) -> &Output {
194        &self.node.outputs[self.output as usize]
195    }
196
197    /// The error this array evaluated to, if it has already failed.
198    pub(crate) fn error(&self) -> Option<Error> {
199        match self.out().slot.get() {
200            Some(Err(e)) => Some(e.clone()),
201            _ => None,
202        }
203    }
204
205    // ── Metadata (never evaluates) ───────────────────────────────────────────
206
207    /// Dimension sizes.
208    pub fn shape(&self) -> &[usize] {
209        &self.out().shape
210    }
211
212    /// Number of dimensions.
213    pub fn ndim(&self) -> usize {
214        self.out().shape.len()
215    }
216
217    /// Number of elements.
218    pub fn size(&self) -> usize {
219        self.out().size
220    }
221
222    /// Element type.
223    pub fn dtype(&self) -> DType {
224        self.out().dtype
225    }
226
227    /// Size of one element in bytes.
228    pub fn itemsize(&self) -> usize {
229        self.dtype().size()
230    }
231
232    /// Size of all elements in bytes.
233    pub fn nbytes(&self) -> usize {
234        self.size() * self.itemsize()
235    }
236
237    /// Per-dimension steps, in elements.
238    pub fn strides(&self) -> &[isize] {
239        &self.out().strides
240    }
241
242    /// Memory-layout flags.
243    pub fn flags(&self) -> Flags {
244        self.out().flags
245    }
246
247    /// The stream the producing operation was recorded on.
248    pub fn stream(&self) -> Stream {
249        self.node.stream
250    }
251
252    /// The device the producing operation runs on.
253    pub fn device(&self) -> Device {
254        self.node.stream.device
255    }
256
257    /// Whether the array has been evaluated (successfully or not).
258    pub fn is_evaluated(&self) -> bool {
259        self.out().slot.get().is_some()
260    }
261
262    /// The operation that produces this array, until it has been evaluated.
263    /// `None` for host data and evaluated arrays.
264    pub fn primitive(&self) -> Option<Primitive> {
265        match &*self.node.lock() {
266            State::Pending { primitive, .. } => Some(primitive.clone()),
267            State::Done => None,
268        }
269    }
270
271    // ── Evaluation and readback ──────────────────────────────────────────────
272
273    /// Evaluates this array, if it has not been evaluated yet.
274    pub fn eval(&self) -> Result<()> {
275        crate::eval(std::slice::from_ref(self))
276    }
277
278    /// The elements in row-major order, evaluating first if needed.
279    pub fn to_vec<T: Element>(&self) -> Result<Vec<T>> {
280        if T::DTYPE != self.dtype() {
281            return Err(Error::DTypeMismatch {
282                op: "to_vec",
283                lhs: self.dtype(),
284                rhs: T::DTYPE,
285            });
286        }
287        self.eval()?;
288        let data = self.data().expect("eval succeeded, so data is set");
289        let out = self.out();
290        if out.size == 0 {
291            Ok(Vec::new())
292        } else if out.flags.row_contiguous || out.size == 1 {
293            let start = data.offset.max(0) as usize * out.dtype.size();
294            Ok(T::read_bytes(
295                &data.buffer.bytes()[start..start + self.nbytes()],
296            ))
297        } else {
298            let gathered = crate::kernels::contiguous(out, data)?;
299            Ok(T::read_bytes(gathered.bytes()))
300        }
301    }
302
303    /// The single element of a one-element array, evaluating first if needed.
304    pub fn item<T: Element>(&self) -> Result<T> {
305        if self.size() != 1 {
306            return Err(Error::InvalidArgument(format!(
307                "item() needs exactly one element, the array has shape {:?}",
308                self.shape()
309            )));
310        }
311        if T::DTYPE != self.dtype() {
312            return Err(Error::DTypeMismatch {
313                op: "item",
314                lhs: self.dtype(),
315                rhs: T::DTYPE,
316            });
317        }
318        Ok(self.to_vec::<T>()?[0])
319    }
320
321    /// The evaluated data, if evaluation succeeded.
322    pub(crate) fn data(&self) -> Option<&Data> {
323        self.out().slot.get().and_then(|r| r.as_ref().ok())
324    }
325}
326
327impl fmt::Debug for Array {
328    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329        let status = match self.out().slot.get() {
330            None => "unevaluated",
331            Some(Ok(_)) => "evaluated",
332            Some(Err(_)) => "failed",
333        };
334        f.debug_struct("Array")
335            .field("shape", &self.shape())
336            .field("dtype", &self.dtype())
337            .field("device", &self.device())
338            .field("status", &status)
339            .finish()
340    }
341}
342
343/// Drops unevaluated graphs iteratively. Recursive destruction would overflow the stack
344/// on long chains: each node's last reference would drop its inputs from inside its own
345/// destructor.
346impl Drop for Node {
347    fn drop(&mut self) {
348        let mut stack = take_inputs(self);
349        while let Some(Array { node, .. }) = stack.pop() {
350            if let Some(mut node) = Arc::into_inner(node) {
351                stack.append(&mut take_inputs(&mut node));
352            }
353        }
354    }
355}
356
357fn take_inputs(node: &mut Node) -> Vec<Array> {
358    let state = node.state.get_mut().unwrap_or_else(PoisonError::into_inner);
359    match std::mem::replace(state, State::Done) {
360        State::Pending { inputs, .. } => inputs,
361        State::Done => Vec::new(),
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn metadata_without_eval() {
371        let a = Array::from_slice(&[1i32, 2, 3, 4, 5, 6], &[2, 3]).unwrap();
372        let c = crate::add(&a, &a).unwrap();
373        assert!(!c.is_evaluated());
374        assert_eq!(c.shape(), &[2, 3]);
375        assert_eq!(c.ndim(), 2);
376        assert_eq!(c.size(), 6);
377        assert_eq!(c.dtype(), DType::I32);
378        assert_eq!(c.itemsize(), 4);
379        assert_eq!(c.nbytes(), 24);
380        assert_eq!(c.strides(), &[3, 1]);
381        assert!(c.flags().row_contiguous);
382        assert_eq!(c.device(), Device::cpu());
383        assert_eq!(c.primitive(), Some(Primitive::Binary(crate::BinaryOp::Add)));
384        assert!(!c.is_evaluated(), "metadata queries must not evaluate");
385    }
386
387    #[test]
388    fn item_on_scalar_and_non_scalar() {
389        assert_eq!(Array::scalar(7u16).item::<u16>().unwrap(), 7);
390        let a = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
391        assert!(matches!(a.item::<f32>(), Err(Error::InvalidArgument(_))));
392        assert!(matches!(
393            Array::scalar(1.0f32).item::<f64>(),
394            Err(Error::DTypeMismatch { .. })
395        ));
396    }
397
398    #[test]
399    fn debug_does_not_evaluate() {
400        let a = Array::scalar(1i8);
401        let c = crate::add(&a, &a).unwrap();
402        assert!(format!("{c:?}").contains("unevaluated"));
403        assert!(!c.is_evaluated());
404    }
405}