Skip to main content

nuvec_core/
eval.rs

1//! Graph evaluation.
2
3use std::sync::Arc;
4
5use crate::array::{Data, Node, NodeMap, NodeSet, State};
6use crate::gpu::GpuEval;
7use crate::kernels::{self, Operand};
8use crate::primitive::Primitive;
9use crate::{Array, DeviceKind, Error, Result, view};
10
11/// Evaluates `arrays` and everything they depend on, and waits for them. Each node runs at
12/// most once, even across threads.
13///
14/// Nodes on the GPU stream run as Metal kernels where one exists, and on the CPU (over the
15/// same unified memory) otherwise. Returns the first error among `arrays`. Errors are
16/// sticky: a failed array, and anything computed from it, fails with the same error on
17/// every later evaluation or readback.
18pub fn eval(arrays: &[Array]) -> Result<()> {
19    schedule(arrays).eval()
20}
21
22/// Starts evaluating `arrays` and returns without waiting for GPU work: reading the arrays
23/// (or evaluating them) later waits for exactly the work they need. As in MLX, use it to
24/// overlap building the next computation with the GPU running this one.
25///
26/// CPU work runs on the calling thread before this returns. Errors are sticky: an error
27/// already known is returned; a GPU error is reported by the first read or evaluation of
28/// an affected array. Submission blocks while too many command buffers are in flight.
29///
30/// ```
31/// use nuvec_core::{Array, Result, async_eval};
32///
33/// # fn main() -> Result<()> {
34/// let x = Array::from_slice(&[1.0f32, 2.0], &[2])?;
35/// let y = &x * 3.0;
36/// async_eval(std::slice::from_ref(&y))?;
37/// // ... build the next computation here ...
38/// assert_eq!(y.to_vec::<f32>()?, [3.0, 6.0]); // waits for `y` only
39/// # Ok(())
40/// # }
41/// ```
42pub fn async_eval(arrays: &[Array]) -> Result<()> {
43    schedule(arrays).async_eval()
44}
45
46/// `arrays` with the work they need put in order, to be started later by
47/// [`Scheduled::async_eval`] or [`Scheduled::eval`] with less delay: ordering a large
48/// graph takes a while, which can then overlap other work, such as the GPU running the
49/// previous step while the next one waits for its result.
50///
51/// The arrays are evaluated as [`async_eval`] or [`eval`] would at the time they are
52/// started; nodes evaluated in between (by another evaluation) are skipped.
53///
54/// ```
55/// use nuvec_core::{Array, Result, schedule};
56///
57/// # fn main() -> Result<()> {
58/// let x = Array::from_slice(&[1.0f32, 2.0], &[2])?;
59/// let y = &(&x * 3.0) + 1.0;
60/// let scheduled = schedule(std::slice::from_ref(&y));
61/// // ... wait for something else here ...
62/// scheduled.async_eval()?;
63/// assert_eq!(y.to_vec::<f32>()?, [4.0, 7.0]);
64/// # Ok(())
65/// # }
66/// ```
67pub fn schedule(arrays: &[Array]) -> Scheduled {
68    Scheduled {
69        arrays: arrays.to_vec(),
70        order: topological_order(arrays),
71    }
72}
73
74/// Arrays whose evaluation is put in order but not started ([`schedule`]).
75pub struct Scheduled {
76    arrays: Vec<Array>,
77    order: Vec<Arc<Node>>,
78}
79
80impl std::fmt::Debug for Scheduled {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("Scheduled")
83            .field("arrays", &self.arrays)
84            .field("nodes", &self.order.len())
85            .finish()
86    }
87}
88
89impl Scheduled {
90    /// The scheduled arrays.
91    pub fn arrays(&self) -> &[Array] {
92        &self.arrays
93    }
94
95    /// Starts evaluating the arrays without waiting for GPU work, as [`async_eval`].
96    pub fn async_eval(self) -> Result<()> {
97        let arrays = self.arrays.clone();
98        self.submit().commit();
99        match arrays.iter().find_map(Array::error) {
100            Some(err) => Err(err),
101            None => Ok(()),
102        }
103    }
104
105    /// Evaluates the arrays and waits for them, as [`eval`].
106    pub fn eval(self) -> Result<()> {
107        let arrays = self.arrays.clone();
108        // Errors are reported through the arrays' slots below.
109        let _ = self.submit().flush();
110        wait(&arrays)
111    }
112
113    /// Encodes or computes every node in order; the caller commits (or flushes) the GPU
114    /// work.
115    fn submit(self) -> GpuEval {
116        let mut gpu = GpuEval::new();
117        for node in self.order {
118            evaluate(&node, &mut gpu);
119            // `node` is released here, so intermediates are freed as soon as their last
120            // consumer has run (or, on the GPU, once their command buffer completes).
121        }
122        gpu
123    }
124}
125
126/// Waits for `arrays`; the first error among them.
127fn wait(arrays: &[Array]) -> Result<()> {
128    for array in arrays {
129        if let Err(err) = array.out().slot.wait() {
130            return Err(err.clone());
131        }
132    }
133    Ok(())
134}
135
136/// The widest breadth-first frontier when ordering a graph (MLX's `MLX_BFS_MAX_WIDTH`).
137const BFS_MAX_WIDTH: usize = 20;
138
139/// Unevaluated nodes reachable from `arrays`, inputs before consumers. Iterative, so graph
140/// depth is limited only by memory.
141///
142/// As MLX's `eval_impl`: a tape is built breadth-first back from the outputs, a node
143/// joining once all its consumers have, and run in reverse. Independent work (the query,
144/// key and value projections of a layer, say) is then encoded side by side, so the GPU
145/// runs it concurrently without barriers between. Past [`BFS_MAX_WIDTH`] pending nodes the
146/// walk goes depth-first, which bounds how many intermediates are alive at once.
147fn topological_order(arrays: &[Array]) -> Vec<Arc<Node>> {
148    // Unevaluated nodes with their unevaluated inputs, depth-first.
149    let mut inputs: NodeMap<*const Node, Vec<Arc<Node>>> = NodeMap::default();
150    let mut done = NodeSet::default();
151    let mut stack: Vec<Arc<Node>> = arrays.iter().map(|a| a.node.clone()).collect();
152    while let Some(node) = stack.pop() {
153        let key = Arc::as_ptr(&node);
154        if inputs.contains_key(&key) || done.contains(&key) {
155            continue;
156        }
157        let pending: Vec<Arc<Node>> = match &*node.lock() {
158            State::Pending { inputs, .. } => inputs.iter().map(|a| a.node.clone()).collect(),
159            State::Retained { .. } | State::Done => {
160                done.insert(key);
161                continue;
162            }
163        };
164        stack.extend(pending.iter().cloned());
165        inputs.insert(key, pending);
166    }
167    for list in inputs.values_mut() {
168        list.retain(|n| !done.contains(&Arc::as_ptr(n)));
169    }
170    let roots: Vec<Arc<Node>> = arrays
171        .iter()
172        .map(|a| a.node.clone())
173        .filter(|n| inputs.contains_key(&Arc::as_ptr(n)))
174        .collect();
175    // Consumers of each node (an input used twice counts twice; the outputs count once
176    // each for the caller).
177    let mut consumers: NodeMap<*const Node, usize> = NodeMap::default();
178    for n in inputs.values().flatten().chain(&roots) {
179        *consumers.entry(Arc::as_ptr(n)).or_default() += 1;
180    }
181    // The tape: `None` stands for the caller, whose inputs are `roots`.
182    let mut tape: Vec<Option<Arc<Node>>> = vec![None];
183    let mut deferred: Vec<(Option<Arc<Node>>, usize)> = Vec::new();
184    let mut next = 0;
185    while !consumers.is_empty() && (next < tape.len() || !deferred.is_empty()) {
186        let (node, mut j) = if next < tape.len() {
187            next += 1;
188            (tape[next - 1].clone(), 0)
189        } else {
190            deferred.pop().expect("checked above")
191        };
192        let list = match &node {
193            Some(n) => &inputs[&Arc::as_ptr(n)],
194            None => &roots,
195        };
196        while j < list.len() {
197            let key = Arc::as_ptr(&list[j]);
198            if consumers.contains_key(&key) {
199                if tape.len() - next >= BFS_MAX_WIDTH {
200                    deferred.push((node.clone(), j));
201                    break;
202                }
203                let left = consumers.get_mut(&key).expect("checked above");
204                *left -= 1;
205                if *left == 0 {
206                    consumers.remove(&key);
207                    tape.push(Some(list[j].clone()));
208                }
209            }
210            j += 1;
211        }
212    }
213    tape.into_iter().rev().flatten().collect()
214}
215
216/// Runs one node whose inputs are all evaluated, stores (or, for GPU work, defers) its
217/// outputs, and detaches it (unless a running transform needs its graph).
218fn evaluate(node: &Arc<Node>, gpu: &mut GpuEval) {
219    let mut state = node.lock();
220    let (primitive, inputs) = match std::mem::replace(&mut *state, State::Done) {
221        State::Pending { primitive, inputs } => (primitive, inputs),
222        evaluated => {
223            *state = evaluated; // evaluated meanwhile by another thread
224            return;
225        }
226    };
227    #[cfg(test)]
228    counter::inc();
229    let mut inputs = inputs;
230    let result = match donation(node, &primitive, &mut inputs, gpu) {
231        Some(base) => in_place(node, &primitive, &inputs, base, gpu),
232        None => compute(node, &primitive, &inputs, gpu),
233    };
234    match result {
235        Ok((outputs, true)) => gpu.defer(node.clone(), outputs),
236        Ok((outputs, false)) => {
237            for (output, data) in node.outputs.iter().zip(outputs) {
238                let _ = output.slot.set(Ok(data));
239            }
240        }
241        Err(err) => {
242            for output in &node.outputs {
243                let _ = output.slot.set(Err(err.clone()));
244            }
245        }
246    }
247    if node.tracer && crate::transforms::tracing() {
248        *state = State::Retained { primitive, inputs };
249        return;
250    }
251    drop(state);
252    drop(inputs); // detach: release the inputs outside the lock
253}
254
255/// The buffer of a `slice_update`'s base, taken over to be written in place (MLX's
256/// donation): only when the evaluation owns the base outright — its node has no other
257/// strong handle (so no other array, view or consumer refers to it) — and its data is the
258/// whole of a writable buffer with no other handle. Ownership proves the write exclusive,
259/// so no reference counting of other users is needed.
260///
261/// The base may be finished, or still in flight in an earlier evaluation: pending
262/// work holds nodes and data only weakly, so the in-flight copy is then the data's only
263/// handle; taking it over (under the lock that publishing takes) means it is never
264/// published. The buffer's fences order the write after the work that produces and reads
265/// it.
266fn donation(
267    node: &Node,
268    primitive: &Primitive,
269    inputs: &mut [Array],
270    gpu: &GpuEval,
271) -> Option<Data> {
272    let device = node.stream.device;
273    if !matches!(primitive, Primitive::SliceUpdate { .. })
274        || node.tracer
275        || device.kind == DeviceKind::Cpu
276        || !gpu.available(device)
277    {
278        return None;
279    }
280    let base = &mut inputs[0].node;
281    if Arc::strong_count(base) != 1 {
282        return None;
283    }
284    let usable = |data: &Data, out: &crate::array::Output, handles: usize| {
285        out.flags.row_contiguous
286            && crate::gpu::writable(data, out)
287            && Arc::strong_count(&data.buffer) == handles
288    };
289    // Finished: no pending work refers to it any more, so it is owned outright.
290    if let Some(base) = Arc::get_mut(base) {
291        let [out] = base.outputs.as_mut_slice() else {
292            return None;
293        };
294        if let Some(Ok(data)) = out.slot.get() {
295            // The slot and (if it was published from in-flight work) the in-flight copy.
296            let handles = 1 + usize::from(out.inflight.get().is_some());
297            if !usable(data, out, handles) {
298                return None;
299            }
300            out.inflight.clear();
301            return match out.slot.take() {
302                Some(Ok(data)) => Some(data),
303                _ => None,
304            };
305        }
306    }
307    // In flight: only weak references from pending work remain besides ours.
308    let [out] = base.outputs.as_slice() else {
309        return None;
310    };
311    if out.slot.get().is_some() {
312        return None;
313    }
314    let mut state = out.inflight.lock();
315    let crate::array::InFlightState::Set(data) = &*state else {
316        return None;
317    };
318    if !usable(data, out, 1) {
319        return None;
320    }
321    match std::mem::replace(&mut *state, crate::array::InFlightState::Donated) {
322        crate::array::InFlightState::Set(data) => Some(data),
323        _ => None,
324    }
325}
326
327/// A `slice_update` written into its donated base.
328fn in_place(
329    node: &Node,
330    primitive: &Primitive,
331    inputs: &[Array],
332    base: Data,
333    gpu: &mut GpuEval,
334) -> Result<(Vec<Data>, bool)> {
335    let (mut pending, mut foreign) = (false, Vec::new());
336    let update = input_data(&inputs[1], gpu, &mut pending, &mut foreign)?;
337    let slices = primitive.dim_slices(node.outputs[0].shape.len())?;
338    let out = &node.outputs[0];
339    let data = gpu.slice_update_in_place(base, out, (inputs[1].out(), &update), &slices)?;
340    Ok((vec![data], true))
341}
342
343/// An input's data: from this evaluation's GPU work, evaluated, or in flight in another
344/// evaluation (then `pending`, and recorded in `foreign`).
345fn input_data<'a>(
346    input: &'a Array,
347    gpu: &mut GpuEval,
348    pending: &mut bool,
349    foreign: &mut Vec<&'a Array>,
350) -> Result<Data> {
351    if let Some(d) = gpu.lookup(&input.node, input.output) {
352        *pending = true;
353        Ok(d)
354    } else if let Some(result) = input.out().slot.get() {
355        result.clone()
356    } else if let Some(d) = input.out().inflight.get() {
357        // Committed by another evaluation on the device timeline: GPU work encoded now
358        // runs after it, so it may read the data at once.
359        *pending = true;
360        foreign.push(input);
361        Ok(d)
362    } else {
363        // Produced by another evaluation (another thread, or an earlier `async_eval`):
364        // submit our own GPU work first, so two evaluations never wait on each other.
365        gpu.commit();
366        input.out().slot.wait().clone()
367    }
368}
369
370/// Computes a node's outputs. The flag is true if they depend on GPU work that has not
371/// completed yet (and must be deferred until it has).
372fn compute(
373    node: &Node,
374    primitive: &Primitive,
375    inputs: &[Array],
376    gpu: &mut GpuEval,
377) -> Result<(Vec<Data>, bool)> {
378    let mut data = Vec::with_capacity(inputs.len());
379    let mut pending = false;
380    // Inputs read from another evaluation's in-flight GPU work.
381    let mut foreign = Vec::new();
382    for input in inputs {
383        data.push(input_data(input, gpu, &mut pending, &mut foreign)?);
384    }
385    let operands: Vec<Operand<'_>> = inputs
386        .iter()
387        .zip(&data)
388        .map(|(a, d)| (a.out(), d))
389        .collect();
390    // Waits until foreign in-flight inputs are complete (for work that does not run on the
391    // device timeline).
392    let await_foreign = || -> Result<()> {
393        for input in &foreign {
394            input.out().slot.wait().clone()?;
395        }
396        Ok(())
397    };
398    if let Some(views) = view_outputs(primitive, &operands)? {
399        // Views do no work: they are published with this evaluation's GPU work (which runs
400        // after any foreign producer) or, without GPU work, once the producers complete.
401        gpu.depend_on(&foreign);
402        return Ok((views, pending));
403    }
404    let device = node.stream.device;
405    if device.kind != DeviceKind::Cpu {
406        if !gpu.available(device) {
407            return Err(Error::UnsupportedDevice(device));
408        }
409        if gpu.supports(primitive, &operands, &node.outputs) {
410            return Ok((gpu.run(primitive, &operands, &node.outputs)?, true));
411        }
412    }
413    // The CPU reads inputs in place: wait for the GPU work that produces them.
414    await_foreign()?;
415    if pending {
416        gpu.flush()?;
417    }
418    Ok((cpu(primitive, &operands)?, false))
419}
420
421/// Outputs of view primitives, which share their input's buffer (on every device), or
422/// `None` for primitives that compute.
423fn view_outputs(primitive: &Primitive, operands: &[Operand<'_>]) -> Result<Option<Vec<Data>>> {
424    let share = |offset| {
425        Some(vec![Data {
426            buffer: operands[0].1.buffer.clone(),
427            offset,
428        }])
429    };
430    Ok(match primitive {
431        Primitive::Transpose(_) | Primitive::Broadcast(_) | Primitive::StopGradient => {
432            share(operands[0].1.offset)
433        }
434        Primitive::Slice { .. } => {
435            let (meta, data) = operands[0];
436            let (_, delta) =
437                view::slice_geometry(&meta.strides, &primitive.dim_slices(meta.shape.len())?);
438            share(data.offset + delta)
439        }
440        Primitive::Custom(call) => Some(
441            operands[call.primals..]
442                .iter()
443                .map(|(_, data)| (*data).clone())
444                .collect(),
445        ),
446        Primitive::Reshape(shape) => {
447            let (meta, data) = operands[0];
448            view::nocopy_reshape(&meta.shape, &meta.strides, shape).and_then(|_| share(data.offset))
449        }
450        _ => None,
451    })
452}
453
454/// Computing primitives on the CPU.
455fn cpu(primitive: &Primitive, operands: &[Operand<'_>]) -> Result<Vec<Data>> {
456    let new = |buffer| Ok(vec![Data::new(buffer)]);
457    match primitive {
458        Primitive::Unary(op) => new(kernels::unary(*op, operands[0])?),
459        Primitive::Binary(op) => new(kernels::binary(*op, operands[0], operands[1])?),
460        Primitive::Select => new(kernels::select(operands[0], operands[1], operands[2])?),
461        Primitive::AsType(dtype) => new(kernels::cast(operands[0], *dtype)?),
462        Primitive::Reduce { op, axes } => new(kernels::reduce(*op, operands[0], axes)?),
463        Primitive::ArgReduce { op, axis } => new(kernels::arg_reduce(*op, operands[0], *axis)?),
464        Primitive::GatherAxis { axis } => {
465            new(kernels::gather_axis(operands[0], operands[1], *axis)?)
466        }
467        Primitive::ScatterAxis { axis, reduce } => new(kernels::scatter_axis(
468            operands[0],
469            operands[1],
470            operands[2],
471            *axis,
472            *reduce,
473        )?),
474        Primitive::Concatenate { axis } => {
475            let mut shape = operands[0].0.shape.clone();
476            shape[*axis] = operands.iter().map(|(m, _)| m.shape[*axis]).sum();
477            new(kernels::concatenate(operands, &shape, *axis)?)
478        }
479        Primitive::Sort { axis } => new(kernels::sort(operands[0], *axis, false)?),
480        Primitive::ArgSort { axis } => new(kernels::sort(operands[0], *axis, true)?),
481        Primitive::Scan {
482            op,
483            axis,
484            reverse,
485            inclusive,
486        } => new(kernels::scan(
487            *op,
488            operands[0],
489            *axis,
490            *reverse,
491            *inclusive,
492        )?),
493        Primitive::Matmul => new(kernels::matmul(operands[0], operands[1])?),
494        Primitive::AddMM { .. } => {
495            let (alpha, beta) = primitive.addmm_factors();
496            new(kernels::addmm(
497                operands[0],
498                operands[1],
499                operands[2],
500                alpha,
501                beta,
502            )?)
503        }
504        Primitive::RandomBits { shape, .. } => {
505            let n: usize = shape.iter().product();
506            new(kernels::random_bits(
507                operands[0],
508                n * primitive_width(primitive),
509            )?)
510        }
511        Primitive::Fast(op) => op.eval_cpu(operands),
512        Primitive::MetalKernel(_) => Err(Error::InvalidArgument(
513            "metal_kernel: custom kernels run on the GPU only".into(),
514        )),
515        Primitive::Compiled(fused) => fused.eval_cpu(operands),
516        Primitive::Quantize { group_size, bits } => {
517            let (packed, scales, biases) =
518                kernels::quantize(operands[0], *group_size, usize::from(*bits))?;
519            Ok(vec![
520                Data::new(packed),
521                Data::new(scales),
522                Data::new(biases),
523            ])
524        }
525        Primitive::Dequantize { group_size, bits } => new(kernels::dequantize(
526            operands[0],
527            operands[1],
528            operands[2],
529            *group_size,
530            usize::from(*bits),
531        )?),
532        Primitive::QuantizedMatmul {
533            group_size,
534            bits,
535            transpose,
536        } => new(kernels::quantized_matmul(
537            operands,
538            *group_size,
539            usize::from(*bits),
540            *transpose,
541        )?),
542        Primitive::SliceUpdate { .. } => new(kernels::slice_update(
543            operands[0],
544            operands[1],
545            &primitive.dim_slices(operands[0].0.shape.len())?,
546        )?),
547        // A reshape that `view_outputs` could not express as a view copies.
548        Primitive::Reshape(_) => new(kernels::contiguous(operands[0].0, operands[0].1)?),
549        Primitive::Transpose(_)
550        | Primitive::Broadcast(_)
551        | Primitive::Slice { .. }
552        | Primitive::StopGradient
553        | Primitive::Custom(_) => {
554            unreachable!("views are handled by view_outputs")
555        }
556        #[cfg(test)]
557        Primitive::AddMultiply => {
558            use crate::primitive::BinaryOp;
559            Ok(vec![
560                Data::new(kernels::binary(BinaryOp::Add, operands[0], operands[1])?),
561                Data::new(kernels::binary(
562                    BinaryOp::Multiply,
563                    operands[0],
564                    operands[1],
565                )?),
566            ])
567        }
568    }
569}
570
571fn primitive_width(primitive: &Primitive) -> usize {
572    match primitive {
573        Primitive::RandomBits { width, .. } => usize::from(*width),
574        _ => unreachable!(),
575    }
576}
577
578/// Per-thread count of evaluated nodes, for run-once tests.
579#[cfg(test)]
580pub(crate) mod counter {
581    use std::cell::Cell;
582
583    thread_local!(static EVALUATED: Cell<usize> = const { Cell::new(0) });
584
585    pub(crate) fn inc() {
586        EVALUATED.set(EVALUATED.get() + 1);
587    }
588    pub(crate) fn take() -> usize {
589        EVALUATED.replace(0)
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use crate::buffer::live;
597
598    #[test]
599    fn shared_node_runs_once() {
600        let x = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
601        let shared = &x + &x;
602        let left = &shared * &x;
603        let right = &shared + &x;
604        let top = &left + &right;
605        counter::take();
606        eval(&[top.clone(), left.clone()]).unwrap();
607        assert_eq!(counter::take(), 4, "shared, left, right, top: once each");
608        top.eval().unwrap();
609        assert_eq!(counter::take(), 0, "already evaluated");
610        assert_eq!(top.to_vec::<f32>().unwrap(), vec![5.0, 14.0]);
611    }
612
613    #[test]
614    fn intermediates_freed() {
615        // The live count is per thread; GPU buffers are released on Metal's completion
616        // thread, so this checks the CPU path.
617        crate::set_default_stream(crate::Stream::default_for(crate::Device::cpu()));
618        let before = live::count();
619        let mut x = Array::scalar(0i64);
620        let one = Array::scalar(1i64);
621        let steps = if cfg!(miri) { 100 } else { 10_000 };
622        for _ in 0..steps {
623            x = &x + &one;
624        }
625        x.eval().unwrap();
626        drop(one);
627        assert_eq!(
628            live::count() - before,
629            1,
630            "only the result's buffer is alive"
631        );
632        assert_eq!(x.item::<i64>().unwrap(), steps as i64);
633    }
634
635    #[test]
636    fn multi_output_once() {
637        let a = Array::from_slice(&[2i32, 3], &[2]).unwrap();
638        let b = Array::from_slice(&[5i32, 7], &[2]).unwrap();
639        let mut outs = Array::from_op(Primitive::AddMultiply, vec![a, b]).unwrap();
640        let product = outs.pop().unwrap();
641        let sum = outs.pop().unwrap();
642        counter::take();
643        eval(&[sum.clone(), product.clone()]).unwrap();
644        assert_eq!(counter::take(), 1);
645        drop(sum);
646        assert_eq!(product.to_vec::<i32>().unwrap(), vec![10, 21]);
647    }
648
649    #[test]
650    fn slice_update_writes_a_strided_region() {
651        // Base: a transposed view; region: rows 0..3 step 2, columns reversed.
652        let base = crate::arange(0.0, 12.0, 1.0, crate::DType::I32)
653            .unwrap()
654            .reshape(&[4, 3])
655            .unwrap()
656            .t()
657            .unwrap(); // [[0,3,6,9],[1,4,7,10],[2,5,8,11]]
658        let update = Array::from_slice(&[-1i32, -2, -3, -4, -5, -6, -7, -8], &[2, 4]).unwrap();
659        let primitive = Primitive::SliceUpdate {
660            start: vec![0, 3],
661            step: vec![2, -1],
662            len: vec![2, 4],
663        };
664        let out = Array::from_op(primitive.clone(), vec![base.clone(), update.clone()])
665            .unwrap()
666            .remove(0);
667        assert_eq!(
668            out.to_vec::<i32>().unwrap(),
669            [-4, -3, -2, -1, 1, 4, 7, 10, -8, -7, -6, -5]
670        );
671        let wrong = Array::from_slice(&[0i32; 3], &[3]).unwrap();
672        assert!(Array::from_op(primitive, vec![base, wrong]).is_err());
673    }
674
675    #[test]
676    fn tracer_nodes_keep_their_graph_only_while_tracing() {
677        let x = Array::scalar(2.0f32);
678        let kept = std::cell::Cell::new(false);
679        let g = crate::grad(|x: &Array| {
680            let y = x * x;
681            y.eval()?;
682            kept.set(matches!(&*y.node.lock(), State::Retained { .. }));
683            Ok(y)
684        });
685        assert_eq!(g(&x).unwrap().item::<f32>().unwrap(), 4.0);
686        assert!(kept.get(), "retained inside a transform");
687        let traced = crate::vjp(
688            |xs| Ok(vec![&xs[0] * &xs[0]]),
689            &[x],
690            &[Array::scalar(1.0f32)],
691        )
692        .unwrap()
693        .0
694        .remove(0);
695        traced.eval().unwrap();
696        assert!(
697            matches!(&*traced.node.lock(), State::Done),
698            "outside a transform"
699        );
700    }
701
702    #[test]
703    fn evaluation_detaches_inputs() {
704        let a = Array::scalar(1u8);
705        let c = &a + &a;
706        assert!(c.primitive().is_some());
707        c.eval().unwrap();
708        assert_eq!(c.primitive(), None);
709        assert_eq!(Arc::strong_count(&a.node), 1, "c no longer references a");
710    }
711
712    /// Positions of nodes in the evaluation order of `arrays`, checked to be topological.
713    fn positions(arrays: &[Array]) -> NodeMap<*const Node, usize> {
714        let order = topological_order(arrays);
715        let at: NodeMap<*const Node, usize> = order
716            .iter()
717            .enumerate()
718            .map(|(i, n)| (Arc::as_ptr(n), i))
719            .collect();
720        assert_eq!(at.len(), order.len(), "each node once");
721        for (i, node) in order.iter().enumerate() {
722            if let State::Pending { inputs, .. } = &*node.lock() {
723                for input in inputs {
724                    if let Some(&j) = at.get(&Arc::as_ptr(&input.node)) {
725                        assert!(j < i, "inputs come first");
726                    }
727                }
728            }
729        }
730        at
731    }
732
733    #[test]
734    fn independent_work_is_ordered_side_by_side() {
735        let x = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
736        let (a1, b1) = (&x + 1.0, &x + 3.0);
737        let (a2, b2) = (&a1 * 2.0, &b1 * 4.0);
738        let top = &a2 + &b2;
739        let at = positions(std::slice::from_ref(&top));
740        let p = |a: &Array| at[&Arc::as_ptr(&a.node)];
741        assert!(
742            p(&a1).max(p(&b1)) < p(&a2).min(p(&b2)),
743            "both chains advance together"
744        );
745        assert_eq!(top.to_vec::<f32>().unwrap(), [20.0, 26.0]);
746    }
747
748    #[test]
749    fn wide_and_shared_graphs_are_ordered() {
750        let x = Array::from_slice(&[1i32], &[1]).unwrap();
751        let one = Array::from_slice(&[1i32], &[1]).unwrap();
752        let shared = &x + &x;
753        // 100 chains of different depths (wider than the breadth-first limit), joined.
754        let mut joined = shared.clone();
755        for i in 0..100 {
756            let mut c = &shared + &Array::from_slice(&[i], &[1]).unwrap();
757            for _ in 0..i % 7 {
758                c = &c * &one;
759            }
760            joined = &joined + &c;
761        }
762        positions(&[joined.clone(), shared.clone(), joined.clone()]);
763        counter::take();
764        eval(&[joined.clone(), shared.clone()]).unwrap();
765        let nodes = 1 + 100 * 2 + (0..100).map(|i| i % 7).sum::<usize>();
766        assert_eq!(counter::take(), nodes, "every node once");
767        assert_eq!(
768            joined.item::<i32>().unwrap(),
769            2 + 100 * 2 + (0..100).sum::<i32>()
770        );
771    }
772
773    #[test]
774    fn scheduled_evaluation_skips_work_done_meanwhile() {
775        let x = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
776        let shared = &x * &x;
777        let top = &shared + &x;
778        let scheduled = schedule(std::slice::from_ref(&top));
779        assert_eq!(scheduled.arrays().len(), 1);
780        counter::take();
781        shared.eval().unwrap();
782        scheduled.eval().unwrap();
783        assert_eq!(counter::take(), 2, "shared once, then top");
784        assert_eq!(top.to_vec::<f32>().unwrap(), [2.0, 6.0]);
785    }
786}