Skip to main content

nuvec_core/
array.rs

1//! The lazy array handle and the graph nodes behind it.
2
3use std::collections::{HashMap, HashSet};
4use std::fmt;
5use std::hash::{BuildHasherDefault, Hasher};
6use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError};
7
8use smallvec::SmallVec;
9
10use crate::buffer::Buffer;
11use crate::primitive::Primitive;
12use crate::shape::{self, Flags, Shape, Strides};
13use crate::{DType, Device, Element, Error, Result, Stream, default_stream};
14
15/// A lazily evaluated n-dimensional array.
16///
17/// Cloning is cheap: clones share the computation and its result. Operations record a
18/// graph node and return immediately; values are computed on [`Array::eval`],
19/// [`crate::eval`], or when read back.
20#[derive(Clone)]
21pub struct Array {
22    pub(crate) node: Arc<Node>,
23    pub(crate) output: u32,
24}
25
26/// One recorded computation, possibly with several outputs.
27pub(crate) struct Node {
28    pub(crate) stream: Stream,
29    /// Whether the node depends on a transform's tracer (see `transforms`). Tracer nodes
30    /// keep their graph when evaluated inside a transform.
31    pub(crate) tracer: bool,
32    pub(crate) outputs: SmallVec<[Output; 1]>,
33    pub(crate) state: Mutex<State>,
34}
35
36/// Metadata and (once evaluated) data of one node output.
37pub(crate) struct Output {
38    pub(crate) dtype: DType,
39    pub(crate) shape: Shape,
40    pub(crate) strides: Strides,
41    pub(crate) flags: Flags,
42    pub(crate) size: usize,
43    /// Set exactly once, when the node is evaluated (or at creation for leaves).
44    pub(crate) slot: OnceLock<Result<Data>>,
45    /// GPU data whose command buffer is committed on the device timeline but may not have
46    /// completed: later GPU work (ordered after it on the device) may read it before `slot` is set.
47    pub(crate) inflight: InFlight,
48}
49
50/// An output's in-flight data (see [`Output::inflight`]). It is published to the slot from
51/// here on completion, unless a consumer took it over first (donation): the lock orders
52/// the two.
53#[derive(Default)]
54pub(crate) struct InFlight(Mutex<InFlightState>);
55
56#[derive(Default)]
57pub(crate) enum InFlightState {
58    #[default]
59    Unset,
60    #[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
61    Set(Data),
62    /// Taken over by a consumer that writes into it in place; never published.
63    Donated,
64}
65
66impl InFlight {
67    pub(crate) fn lock(&self) -> MutexGuard<'_, InFlightState> {
68        self.0.lock().unwrap_or_else(PoisonError::into_inner)
69    }
70
71    /// Exposes `data` (once; only GPU work has in-flight data).
72    #[cfg_attr(not(all(target_os = "macos", not(miri))), allow(dead_code))]
73    pub(crate) fn set(&self, data: Data) {
74        let mut state = self.lock();
75        if matches!(*state, InFlightState::Unset) {
76            *state = InFlightState::Set(data);
77        }
78    }
79
80    /// The data, if exposed and not donated.
81    pub(crate) fn get(&self) -> Option<Data> {
82        match &*self.lock() {
83            InFlightState::Set(data) => Some(data.clone()),
84            _ => None,
85        }
86    }
87
88    /// Drops the data (the slot holds it now), unless donated.
89    pub(crate) fn clear(&mut self) {
90        let state = self.0.get_mut().unwrap_or_else(PoisonError::into_inner);
91        if matches!(state, InFlightState::Set(_)) {
92            *state = InFlightState::Unset;
93        }
94    }
95}
96
97/// Evaluated data: elements of a (possibly shared) buffer, starting at `offset` and laid
98/// out by the output's strides. Views share their input's buffer.
99#[derive(Clone)]
100pub(crate) struct Data {
101    pub(crate) buffer: Arc<Buffer>,
102    /// Position of the first element, in elements.
103    pub(crate) offset: isize,
104}
105
106impl Data {
107    pub(crate) fn new(buffer: Buffer) -> Self {
108        Self {
109            buffer: Arc::new(buffer),
110            offset: 0,
111        }
112    }
113}
114
115/// Whether a node still has to run. `Done` nodes have dropped their inputs (detach);
116/// `Retained` nodes are evaluated but keep their graph for a running transform.
117pub(crate) enum State {
118    Pending {
119        primitive: Primitive,
120        inputs: Vec<Array>,
121    },
122    Retained {
123        primitive: Primitive,
124        inputs: Vec<Array>,
125    },
126    Done,
127}
128
129/// Hashes node addresses, the keys of the graph walks' sets and maps. Addresses are
130/// unique and not attacker-controlled, so a multiplicative hash replaces SipHash.
131#[derive(Default, Clone, Copy)]
132pub(crate) struct NodeHasher(u64);
133
134impl Hasher for NodeHasher {
135    fn finish(&self) -> u64 {
136        // Move the well-mixed high bits of the product to the low bits the table indexes
137        // with.
138        self.0.rotate_left(26)
139    }
140    fn write(&mut self, bytes: &[u8]) {
141        for &b in bytes {
142            self.write_u64(u64::from(b));
143        }
144    }
145    fn write_u64(&mut self, n: u64) {
146        self.0 = (self.0.rotate_left(5) ^ n).wrapping_mul(0x9E37_79B9_7F4A_7C15);
147    }
148    fn write_u32(&mut self, n: u32) {
149        self.write_u64(u64::from(n));
150    }
151    fn write_usize(&mut self, n: usize) {
152        self.write_u64(n as u64);
153    }
154}
155
156/// A set of nodes by address.
157pub(crate) type NodeSet = HashSet<*const Node, BuildHasherDefault<NodeHasher>>;
158/// A map keyed by node address (optionally with an output index).
159pub(crate) type NodeMap<K, V> = HashMap<K, V, BuildHasherDefault<NodeHasher>>;
160
161const _: () = {
162    const fn assert_send_sync<T: Send + Sync>() {}
163    assert_send_sync::<Array>();
164};
165
166impl Node {
167    pub(crate) fn lock(&self) -> MutexGuard<'_, State> {
168        self.state.lock().unwrap_or_else(PoisonError::into_inner)
169    }
170}
171
172impl Output {
173    pub(crate) fn new(
174        dtype: DType,
175        shape: Shape,
176        strides: Option<Strides>,
177        slot: OnceLock<Result<Data>>,
178    ) -> Result<Self> {
179        shape::checked_nbytes(&shape, dtype.size())?;
180        let size = shape::checked_size(&shape)?;
181        let strides = strides.unwrap_or_else(|| shape::row_contiguous_strides(&shape));
182        let flags = shape::flags_for(&shape, &strides);
183        Ok(Self {
184            dtype,
185            shape,
186            strides,
187            flags,
188            size,
189            slot,
190            inflight: InFlight::default(),
191        })
192    }
193}
194
195impl Array {
196    // ── Construction ─────────────────────────────────────────────────────────
197
198    /// An array holding `data` with the given `shape`.
199    ///
200    /// Fails with [`Error::SizeMismatch`] if `data.len()` is not the shape's element count.
201    pub fn from_slice<T: Element>(data: &[T], shape: &[usize]) -> Result<Self> {
202        let expected = shape::checked_size(shape)?;
203        if data.len() != expected {
204            return Err(Error::SizeMismatch {
205                expected,
206                actual: data.len(),
207            });
208        }
209        let mut buffer = Buffer::try_alloc(shape::checked_nbytes(shape, T::DTYPE.size())?)?;
210        T::write_bytes(data, buffer.bytes_mut());
211        Self::leaf(Ok(Data::new(buffer)), T::DTYPE, shape.into())
212    }
213
214    /// An array of the given `shape` with every element set to `value`.
215    pub fn full<T: Element>(shape: &[usize], value: T) -> Result<Self> {
216        let itemsize = T::DTYPE.size();
217        let mut buffer = Buffer::try_alloc(shape::checked_nbytes(shape, itemsize)?)?;
218        let mut element = vec![0u8; itemsize];
219        T::write_bytes(&[value], &mut element);
220        for chunk in buffer.bytes_mut().chunks_exact_mut(itemsize) {
221            chunk.copy_from_slice(&element);
222        }
223        Self::leaf(Ok(Data::new(buffer)), T::DTYPE, shape.into())
224    }
225
226    /// A zero-dimensional array holding `value`.
227    pub fn scalar<T: Element>(value: T) -> Self {
228        Self::from_slice(&[value], &[]).expect("a scalar always matches its shape")
229    }
230
231    /// An evaluated array over a row-contiguous `buffer`.
232    pub(crate) fn from_buffer(buffer: Buffer, dtype: DType, shape: &[usize]) -> Result<Self> {
233        Self::leaf(Ok(Data::new(buffer)), dtype, shape.into())
234    }
235
236    /// A node that is already evaluated, holding `data` (or a sticky error).
237    pub(crate) fn leaf(data: Result<Data>, dtype: DType, shape: Shape) -> Result<Self> {
238        let output = Output::new(dtype, shape, None, OnceLock::from(data))?;
239        Ok(Self::from_node(Node {
240            stream: default_stream(),
241            tracer: false,
242            outputs: SmallVec::from_buf([output]),
243            state: Mutex::new(State::Done),
244        }))
245    }
246
247    /// An array whose evaluation returns `error`. Used where an error cannot be returned
248    /// directly, such as from operators.
249    pub(crate) fn failed(error: Error, dtype: DType, shape: &[usize]) -> Self {
250        Self::leaf(Err(error.clone()), dtype, shape.into())
251            .unwrap_or_else(|_| Self::leaf(Err(error), dtype, Shape::new()).expect("scalar shape"))
252    }
253
254    /// Records `primitive` applied to `inputs` and returns one array per output.
255    ///
256    /// Inputs that already failed short-circuit with their error.
257    pub(crate) fn from_op(primitive: Primitive, inputs: Vec<Array>) -> Result<Vec<Array>> {
258        let tracer = inputs.iter().any(|a| a.node.tracer);
259        Self::record(primitive, inputs, tracer)
260    }
261
262    /// [`Array::from_op`] with an explicit tracer flag.
263    pub(crate) fn record(
264        primitive: Primitive,
265        inputs: Vec<Array>,
266        tracer: bool,
267    ) -> Result<Vec<Array>> {
268        if let Some(err) = inputs.iter().find_map(Array::error) {
269            return Err(err);
270        }
271        Self::record_unchecked(primitive, inputs, tracer)
272    }
273
274    /// [`Array::record`] without the failed-input check: the node fails only when
275    /// evaluated. Used for `vmap` placeholders.
276    pub(crate) fn record_unchecked(
277        primitive: Primitive,
278        inputs: Vec<Array>,
279        tracer: bool,
280    ) -> Result<Vec<Array>> {
281        let outputs = primitive
282            .output_specs(&inputs)?
283            .into_iter()
284            .map(|spec| Output::new(spec.dtype, spec.shape, spec.strides, OnceLock::new()))
285            .collect::<Result<SmallVec<_>>>()?;
286        let count = outputs.len() as u32;
287        let node = Arc::new(Node {
288            stream: default_stream(),
289            tracer,
290            outputs,
291            state: Mutex::new(State::Pending { primitive, inputs }),
292        });
293        Ok((0..count)
294            .map(|output| Self {
295                node: node.clone(),
296                output,
297            })
298            .collect())
299    }
300
301    fn from_node(node: Node) -> Self {
302        Self {
303            node: Arc::new(node),
304            output: 0,
305        }
306    }
307
308    // ── Graph (for graph transformations such as `compile`) ──────────────────
309
310    /// Records `primitive` applied to `inputs`, with the same validation as the operations,
311    /// and returns one array per output. Inputs that already failed short-circuit with their
312    /// error.
313    pub fn from_primitive(primitive: Primitive, inputs: Vec<Array>) -> Result<Vec<Array>> {
314        Self::from_op(primitive, inputs)
315    }
316
317    /// A placeholder with this dtype and shape, standing for an input while a function is
318    /// traced (as in MLX): operations can be recorded on it, but evaluating anything that
319    /// depends on it fails. Placeholders are tracers.
320    pub fn placeholder(dtype: DType, shape: &[usize]) -> Result<Array> {
321        let failed = Self::failed(
322            Error::InvalidArgument(
323                "arrays cannot be evaluated while a function is traced (compile)".into(),
324            ),
325            dtype,
326            &[],
327        );
328        let primitive = Primitive::Broadcast(shape.to_vec());
329        Ok(Self::record_unchecked(primitive, vec![failed], true)?.swap_remove(0))
330    }
331
332    /// The arrays this one is computed from, until it has been evaluated. Empty for host
333    /// data and evaluated arrays.
334    pub fn inputs(&self) -> Vec<Array> {
335        match &*self.node.lock() {
336            State::Pending { inputs, .. } | State::Retained { inputs, .. } => inputs.clone(),
337            State::Done => Vec::new(),
338        }
339    }
340
341    /// Every output of the operation that produces this array, in order (this array is
342    /// the one at [`Array::output_index`]).
343    pub fn siblings(&self) -> Vec<Array> {
344        (0..self.node.outputs.len() as u32)
345            .map(|output| Self {
346                node: self.node.clone(),
347                output,
348            })
349            .collect()
350    }
351
352    /// Which output of its operation this array is.
353    pub fn output_index(&self) -> usize {
354        self.output as usize
355    }
356
357    /// Identifies the operation that produces this array (shared by its siblings) for as
358    /// long as the array is alive.
359    pub fn id(&self) -> usize {
360        Arc::as_ptr(&self.node) as usize
361    }
362
363    /// Whether the array depends on a transform's tracer or a placeholder, that is, it is
364    /// being traced by `grad`, `vjp`, `jvp`, `vmap` or `compile`.
365    pub fn is_tracer(&self) -> bool {
366        self.node.tracer
367    }
368
369    pub(crate) fn out(&self) -> &Output {
370        &self.node.outputs[self.output as usize]
371    }
372
373    /// The error this array evaluated to, if it has already failed.
374    pub(crate) fn error(&self) -> Option<Error> {
375        match self.out().slot.get() {
376            Some(Err(e)) => Some(e.clone()),
377            _ => None,
378        }
379    }
380
381    // ── Metadata (never evaluates) ───────────────────────────────────────────
382
383    /// Dimension sizes.
384    pub fn shape(&self) -> &[usize] {
385        &self.out().shape
386    }
387
388    /// Number of dimensions.
389    pub fn ndim(&self) -> usize {
390        self.out().shape.len()
391    }
392
393    /// Number of elements.
394    pub fn size(&self) -> usize {
395        self.out().size
396    }
397
398    /// Element type.
399    pub fn dtype(&self) -> DType {
400        self.out().dtype
401    }
402
403    /// Size of one element in bytes.
404    pub fn itemsize(&self) -> usize {
405        self.dtype().size()
406    }
407
408    /// Size of all elements in bytes.
409    pub fn nbytes(&self) -> usize {
410        self.size() * self.itemsize()
411    }
412
413    /// Per-dimension steps, in elements.
414    pub fn strides(&self) -> &[isize] {
415        &self.out().strides
416    }
417
418    /// Memory-layout flags.
419    pub fn flags(&self) -> Flags {
420        self.out().flags
421    }
422
423    /// The stream the producing operation was recorded on.
424    pub fn stream(&self) -> Stream {
425        self.node.stream
426    }
427
428    /// The device the producing operation runs on.
429    pub fn device(&self) -> Device {
430        self.node.stream.device
431    }
432
433    /// Whether the array has been evaluated (successfully or not).
434    pub fn is_evaluated(&self) -> bool {
435        self.out().slot.get().is_some()
436    }
437
438    /// The operation that produces this array, until it has been evaluated.
439    /// `None` for host data and evaluated arrays.
440    pub fn primitive(&self) -> Option<Primitive> {
441        match &*self.node.lock() {
442            State::Pending { primitive, .. } => Some(primitive.clone()),
443            State::Retained { .. } | State::Done => None,
444        }
445    }
446
447    // ── Evaluation and readback ──────────────────────────────────────────────
448
449    /// Evaluates this array, if it has not been evaluated yet.
450    pub fn eval(&self) -> Result<()> {
451        crate::eval(std::slice::from_ref(self))
452    }
453
454    /// The elements in row-major order, evaluating first if needed.
455    pub fn to_vec<T: Element>(&self) -> Result<Vec<T>> {
456        if T::DTYPE != self.dtype() {
457            return Err(Error::DTypeMismatch {
458                op: "to_vec",
459                lhs: self.dtype(),
460                rhs: T::DTYPE,
461            });
462        }
463        self.eval()?;
464        let data = self.data().expect("eval succeeded, so data is set");
465        let out = self.out();
466        if out.size == 0 {
467            Ok(Vec::new())
468        } else if out.flags.row_contiguous || out.size == 1 {
469            let start = data.offset.max(0) as usize * out.dtype.size();
470            Ok(T::read_bytes(
471                &data.buffer.bytes()[start..start + self.nbytes()],
472            ))
473        } else {
474            let gathered = crate::kernels::contiguous(out, data)?;
475            Ok(T::read_bytes(gathered.bytes()))
476        }
477    }
478
479    /// The single element of a one-element array, evaluating first if needed.
480    pub fn item<T: Element>(&self) -> Result<T> {
481        if self.size() != 1 {
482            return Err(Error::InvalidArgument(format!(
483                "item() needs exactly one element, the array has shape {:?}",
484                self.shape()
485            )));
486        }
487        if T::DTYPE != self.dtype() {
488            return Err(Error::DTypeMismatch {
489                op: "item",
490                lhs: self.dtype(),
491                rhs: T::DTYPE,
492            });
493        }
494        Ok(self.to_vec::<T>()?[0])
495    }
496
497    /// The evaluated data, if evaluation succeeded.
498    pub(crate) fn data(&self) -> Option<&Data> {
499        self.out().slot.get().and_then(|r| r.as_ref().ok())
500    }
501}
502
503impl fmt::Debug for Array {
504    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505        let status = match self.out().slot.get() {
506            None => "unevaluated",
507            Some(Ok(_)) => "evaluated",
508            Some(Err(_)) => "failed",
509        };
510        f.debug_struct("Array")
511            .field("shape", &self.shape())
512            .field("dtype", &self.dtype())
513            .field("device", &self.device())
514            .field("status", &status)
515            .finish()
516    }
517}
518
519/// Drops unevaluated graphs iteratively. Recursive destruction would overflow the stack
520/// on long chains: each node's last reference would drop its inputs from inside its own
521/// destructor.
522impl Drop for Node {
523    fn drop(&mut self) {
524        let mut stack = take_inputs(self);
525        while let Some(Array { node, .. }) = stack.pop() {
526            if let Some(mut node) = Arc::into_inner(node) {
527                stack.append(&mut take_inputs(&mut node));
528            }
529        }
530    }
531}
532
533fn take_inputs(node: &mut Node) -> Vec<Array> {
534    let state = node.state.get_mut().unwrap_or_else(PoisonError::into_inner);
535    match std::mem::replace(state, State::Done) {
536        State::Pending { inputs, .. } | State::Retained { inputs, .. } => inputs,
537        State::Done => Vec::new(),
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn graph_accessors_describe_pending_nodes() {
547        let a = Array::from_slice(&[1i32, 2], &[2]).unwrap();
548        let b = Array::from_slice(&[5i32, 7], &[2]).unwrap();
549        let outs = Array::from_primitive(Primitive::AddMultiply, vec![a.clone(), b]).unwrap();
550        let (sum, product) = (&outs[0], &outs[1]);
551        assert_eq!(sum.id(), product.id(), "siblings share their operation");
552        assert_ne!(sum.id(), a.id());
553        assert_eq!((sum.output_index(), product.output_index()), (0, 1));
554        assert_eq!(product.siblings().len(), 2);
555        assert_eq!(sum.inputs()[0].id(), a.id());
556        assert!(a.inputs().is_empty(), "host data has no inputs");
557        assert!(!sum.is_tracer());
558        product.eval().unwrap();
559        assert!(sum.inputs().is_empty(), "evaluated: detached");
560
561        let p = Array::placeholder(DType::F32, &[3]).unwrap();
562        let q = &p + 1.0f32;
563        assert!(p.is_tracer() && q.is_tracer());
564        assert_eq!(q.shape(), [3]);
565        assert!(q.eval().is_err(), "placeholders cannot be evaluated");
566    }
567
568    #[test]
569    fn node_hasher_spreads_aligned_addresses() {
570        use std::hash::BuildHasher;
571        let build = BuildHasherDefault::<NodeHasher>::default();
572        // 336-byte nodes, as allocated: every low table index should be reachable.
573        let buckets: HashSet<u64> = (0..1024u64)
574            .map(|i| {
575                build.hash_one(std::ptr::without_provenance::<Node>(
576                    0x6000_0000_0000 + i as usize * 336,
577                )) & 1023
578            })
579            .collect();
580        // A random hash fills about 1 - 1/e ≈ 63% of them.
581        assert!(
582            buckets.len() > 600,
583            "only {} of 1024 buckets used",
584            buckets.len()
585        );
586    }
587
588    #[test]
589    fn metadata_without_eval() {
590        let a = Array::from_slice(&[1i32, 2, 3, 4, 5, 6], &[2, 3]).unwrap();
591        let c = crate::add(&a, &a).unwrap();
592        assert!(!c.is_evaluated());
593        assert_eq!(c.shape(), &[2, 3]);
594        assert_eq!(c.ndim(), 2);
595        assert_eq!(c.size(), 6);
596        assert_eq!(c.dtype(), DType::I32);
597        assert_eq!(c.itemsize(), 4);
598        assert_eq!(c.nbytes(), 24);
599        assert_eq!(c.strides(), &[3, 1]);
600        assert!(c.flags().row_contiguous);
601        assert_eq!(c.device(), crate::default_device());
602        assert_eq!(c.primitive(), Some(Primitive::Binary(crate::BinaryOp::Add)));
603        assert!(!c.is_evaluated(), "metadata queries must not evaluate");
604    }
605
606    #[test]
607    fn item_on_scalar_and_non_scalar() {
608        assert_eq!(Array::scalar(7u16).item::<u16>().unwrap(), 7);
609        let a = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
610        assert!(matches!(a.item::<f32>(), Err(Error::InvalidArgument(_))));
611        assert!(matches!(
612            Array::scalar(1.0f32).item::<f64>(),
613            Err(Error::DTypeMismatch { .. })
614        ));
615    }
616
617    #[test]
618    fn debug_does_not_evaluate() {
619        let a = Array::scalar(1i8);
620        let c = crate::add(&a, &a).unwrap();
621        assert!(format!("{c:?}").contains("unevaluated"));
622        assert!(!c.is_evaluated());
623    }
624}