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    let writes_base = matches!(
274        primitive,
275        Primitive::SliceUpdate { .. }
276            | Primitive::Fast(crate::fast::FastOp::NormRope { cache: true, .. })
277    );
278    if !writes_base || node.tracer || device.kind == DeviceKind::Cpu || !gpu.available(device) {
279        return None;
280    }
281    let base = &mut inputs[0].node;
282    if Arc::strong_count(base) != 1 {
283        return None;
284    }
285    let usable = |data: &Data, out: &crate::array::Output, handles: usize| {
286        out.flags.row_contiguous
287            && crate::gpu::writable(data, out)
288            && Arc::strong_count(&data.buffer) == handles
289    };
290    // Finished: no pending work refers to it any more, so it is owned outright.
291    if let Some(base) = Arc::get_mut(base) {
292        let [out] = base.outputs.as_mut_slice() else {
293            return None;
294        };
295        if let Some(Ok(data)) = out.slot.get() {
296            // The slot and (if it was published from in-flight work) the in-flight copy.
297            let handles = 1 + usize::from(out.inflight.get().is_some());
298            if !usable(data, out, handles) {
299                return None;
300            }
301            out.inflight.clear();
302            return match out.slot.take() {
303                Some(Ok(data)) => Some(data),
304                _ => None,
305            };
306        }
307    }
308    // In flight: only weak references from pending work remain besides ours.
309    let [out] = base.outputs.as_slice() else {
310        return None;
311    };
312    if out.slot.get().is_some() {
313        return None;
314    }
315    let mut state = out.inflight.lock();
316    let crate::array::InFlightState::Set(data) = &*state else {
317        return None;
318    };
319    if !usable(data, out, 1) {
320        return None;
321    }
322    match std::mem::replace(&mut *state, crate::array::InFlightState::Donated) {
323        crate::array::InFlightState::Set(data) => Some(data),
324        _ => None,
325    }
326}
327
328/// A `slice_update` (or a `norm_rope` into a cache) written into its donated base.
329fn in_place(
330    node: &Node,
331    primitive: &Primitive,
332    inputs: &[Array],
333    base: Data,
334    gpu: &mut GpuEval,
335) -> Result<(Vec<Data>, bool)> {
336    let (mut pending, mut foreign) = (false, Vec::new());
337    let rest = inputs[1..]
338        .iter()
339        .map(|a| input_data(a, gpu, &mut pending, &mut foreign))
340        .collect::<Result<Vec<_>>>()?;
341    let operands: Vec<Operand<'_>> = inputs[1..].iter().map(Array::out).zip(&rest).collect();
342    let out = &node.outputs[0];
343    let data = match primitive {
344        Primitive::Fast(op) => gpu.norm_rope_in_place(base, op, &operands, out)?,
345        _ => {
346            let slices = primitive.dim_slices(out.shape.len())?;
347            gpu.slice_update_in_place(base, out, operands[0], &slices)?
348        }
349    };
350    Ok((vec![data], true))
351}
352
353/// An input's data: from this evaluation's GPU work, evaluated, or in flight in another
354/// evaluation (then `pending`, and recorded in `foreign`).
355fn input_data<'a>(
356    input: &'a Array,
357    gpu: &mut GpuEval,
358    pending: &mut bool,
359    foreign: &mut Vec<&'a Array>,
360) -> Result<Data> {
361    if let Some(d) = gpu.lookup(&input.node, input.output) {
362        *pending = true;
363        Ok(d)
364    } else if let Some(result) = input.out().slot.get() {
365        result.clone()
366    } else if let Some(d) = input.out().inflight.get() {
367        // Committed by another evaluation on the device timeline: GPU work encoded now
368        // runs after it, so it may read the data at once.
369        *pending = true;
370        foreign.push(input);
371        Ok(d)
372    } else {
373        // Produced by another evaluation (another thread, or an earlier `async_eval`):
374        // submit our own GPU work first, so two evaluations never wait on each other.
375        gpu.commit();
376        input.out().slot.wait().clone()
377    }
378}
379
380/// Computes a node's outputs. The flag is true if they depend on GPU work that has not
381/// completed yet (and must be deferred until it has).
382fn compute(
383    node: &Node,
384    primitive: &Primitive,
385    inputs: &[Array],
386    gpu: &mut GpuEval,
387) -> Result<(Vec<Data>, bool)> {
388    let mut data = Vec::with_capacity(inputs.len());
389    let mut pending = false;
390    // Inputs read from another evaluation's in-flight GPU work.
391    let mut foreign = Vec::new();
392    for input in inputs {
393        data.push(input_data(input, gpu, &mut pending, &mut foreign)?);
394    }
395    let operands: Vec<Operand<'_>> = inputs
396        .iter()
397        .zip(&data)
398        .map(|(a, d)| (a.out(), d))
399        .collect();
400    // Waits until foreign in-flight inputs are complete (for work that does not run on the
401    // device timeline).
402    let await_foreign = || -> Result<()> {
403        for input in &foreign {
404            input.out().slot.wait().clone()?;
405        }
406        Ok(())
407    };
408    if let Some(views) = view_outputs(primitive, &operands)? {
409        // Views do no work: they are published with this evaluation's GPU work (which runs
410        // after any foreign producer) or, without GPU work, once the producers complete.
411        gpu.depend_on(&foreign);
412        return Ok((views, pending));
413    }
414    let device = node.stream.device;
415    if device.kind != DeviceKind::Cpu {
416        if !gpu.available(device) {
417            return Err(Error::UnsupportedDevice(device));
418        }
419        if gpu.supports(primitive, &operands, &node.outputs) {
420            return Ok((gpu.run(primitive, &operands, &node.outputs)?, true));
421        }
422    }
423    // The CPU reads inputs in place: wait for the GPU work that produces them.
424    await_foreign()?;
425    if pending {
426        gpu.flush()?;
427    }
428    Ok((cpu(primitive, &operands)?, false))
429}
430
431/// Outputs of view primitives, which share their input's buffer (on every device), or
432/// `None` for primitives that compute.
433fn view_outputs(primitive: &Primitive, operands: &[Operand<'_>]) -> Result<Option<Vec<Data>>> {
434    let share = |offset| {
435        Some(vec![Data {
436            buffer: operands[0].1.buffer.clone(),
437            offset,
438        }])
439    };
440    Ok(match primitive {
441        Primitive::Transpose(_) | Primitive::Broadcast(_) | Primitive::StopGradient => {
442            share(operands[0].1.offset)
443        }
444        Primitive::Slice { .. } => {
445            let (meta, data) = operands[0];
446            let (_, delta) =
447                view::slice_geometry(&meta.strides, &primitive.dim_slices(meta.shape.len())?);
448            share(data.offset + delta)
449        }
450        Primitive::Custom(call) => Some(
451            operands[call.primals..]
452                .iter()
453                .map(|(_, data)| (*data).clone())
454                .collect(),
455        ),
456        Primitive::Reshape(shape) => {
457            let (meta, data) = operands[0];
458            view::nocopy_reshape(&meta.shape, &meta.strides, shape).and_then(|_| share(data.offset))
459        }
460        _ => None,
461    })
462}
463
464/// Computing primitives on the CPU.
465fn cpu(primitive: &Primitive, operands: &[Operand<'_>]) -> Result<Vec<Data>> {
466    let new = |buffer| Ok(vec![Data::new(buffer)]);
467    match primitive {
468        Primitive::Unary(op) => new(kernels::unary(*op, operands[0])?),
469        Primitive::Binary(op) => new(kernels::binary(*op, operands[0], operands[1])?),
470        Primitive::Select => new(kernels::select(operands[0], operands[1], operands[2])?),
471        Primitive::AsType(dtype) => new(kernels::cast(operands[0], *dtype)?),
472        Primitive::Reduce { op, axes } => new(kernels::reduce(*op, operands[0], axes)?),
473        Primitive::ArgReduce { op, axis } => new(kernels::arg_reduce(*op, operands[0], *axis)?),
474        Primitive::GatherAxis { axis } => {
475            new(kernels::gather_axis(operands[0], operands[1], *axis)?)
476        }
477        Primitive::ScatterAxis { axis, reduce } => new(kernels::scatter_axis(
478            operands[0],
479            operands[1],
480            operands[2],
481            *axis,
482            *reduce,
483        )?),
484        Primitive::Concatenate { axis } => {
485            let mut shape = operands[0].0.shape.clone();
486            shape[*axis] = operands.iter().map(|(m, _)| m.shape[*axis]).sum();
487            new(kernels::concatenate(operands, &shape, *axis)?)
488        }
489        Primitive::Sort { axis } => new(kernels::sort(operands[0], *axis, false)?),
490        Primitive::ArgSort { axis } => new(kernels::sort(operands[0], *axis, true)?),
491        Primitive::Scan {
492            op,
493            axis,
494            reverse,
495            inclusive,
496        } => new(kernels::scan(
497            *op,
498            operands[0],
499            *axis,
500            *reverse,
501            *inclusive,
502        )?),
503        Primitive::Matmul => new(kernels::matmul(operands[0], operands[1])?),
504        Primitive::AddMM { .. } => {
505            let (alpha, beta) = primitive.addmm_factors();
506            new(kernels::addmm(
507                operands[0],
508                operands[1],
509                operands[2],
510                alpha,
511                beta,
512            )?)
513        }
514        Primitive::RandomBits { shape, .. } => {
515            let n: usize = shape.iter().product();
516            new(kernels::random_bits(
517                operands[0],
518                n * primitive_width(primitive),
519            )?)
520        }
521        Primitive::Fast(op) => op.eval_cpu(operands),
522        Primitive::MetalKernel(_) => Err(Error::InvalidArgument(
523            "metal_kernel: custom kernels run on the GPU only".into(),
524        )),
525        Primitive::Compiled(fused) => fused.eval_cpu(operands),
526        Primitive::Quantize { group_size, bits } => {
527            let (packed, scales, biases) =
528                kernels::quantize(operands[0], *group_size, usize::from(*bits))?;
529            Ok(vec![
530                Data::new(packed),
531                Data::new(scales),
532                Data::new(biases),
533            ])
534        }
535        Primitive::Dequantize { group_size, bits } => new(kernels::dequantize(
536            operands[0],
537            operands[1],
538            operands[2],
539            *group_size,
540            usize::from(*bits),
541        )?),
542        Primitive::QuantizedMatmul {
543            group_size,
544            bits,
545            transpose,
546        } => new(kernels::quantized_matmul(
547            operands,
548            *group_size,
549            usize::from(*bits),
550            *transpose,
551        )?),
552        Primitive::SliceUpdate { .. } => new(kernels::slice_update(
553            operands[0],
554            operands[1],
555            &primitive.dim_slices(operands[0].0.shape.len())?,
556        )?),
557        // A reshape that `view_outputs` could not express as a view copies.
558        Primitive::Reshape(_) => new(kernels::contiguous(operands[0].0, operands[0].1)?),
559        Primitive::Transpose(_)
560        | Primitive::Broadcast(_)
561        | Primitive::Slice { .. }
562        | Primitive::StopGradient
563        | Primitive::Custom(_) => {
564            unreachable!("views are handled by view_outputs")
565        }
566        #[cfg(test)]
567        Primitive::AddMultiply => {
568            use crate::primitive::BinaryOp;
569            Ok(vec![
570                Data::new(kernels::binary(BinaryOp::Add, operands[0], operands[1])?),
571                Data::new(kernels::binary(
572                    BinaryOp::Multiply,
573                    operands[0],
574                    operands[1],
575                )?),
576            ])
577        }
578    }
579}
580
581fn primitive_width(primitive: &Primitive) -> usize {
582    match primitive {
583        Primitive::RandomBits { width, .. } => usize::from(*width),
584        _ => unreachable!(),
585    }
586}
587
588/// Per-thread count of evaluated nodes, for run-once tests.
589#[cfg(test)]
590pub(crate) mod counter {
591    use std::cell::Cell;
592
593    thread_local!(static EVALUATED: Cell<usize> = const { Cell::new(0) });
594
595    pub(crate) fn inc() {
596        EVALUATED.set(EVALUATED.get() + 1);
597    }
598    pub(crate) fn take() -> usize {
599        EVALUATED.replace(0)
600    }
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606    use crate::buffer::live;
607
608    #[test]
609    fn shared_node_runs_once() {
610        let x = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
611        let shared = &x + &x;
612        let left = &shared * &x;
613        let right = &shared + &x;
614        let top = &left + &right;
615        counter::take();
616        eval(&[top.clone(), left.clone()]).unwrap();
617        assert_eq!(counter::take(), 4, "shared, left, right, top: once each");
618        top.eval().unwrap();
619        assert_eq!(counter::take(), 0, "already evaluated");
620        assert_eq!(top.to_vec::<f32>().unwrap(), vec![5.0, 14.0]);
621    }
622
623    #[test]
624    fn intermediates_freed() {
625        // The live count is per thread; GPU buffers are released on Metal's completion
626        // thread, so this checks the CPU path.
627        crate::set_default_stream(crate::Stream::default_for(crate::Device::cpu()));
628        let before = live::count();
629        let mut x = Array::scalar(0i64);
630        let one = Array::scalar(1i64);
631        let steps = if cfg!(miri) { 100 } else { 10_000 };
632        for _ in 0..steps {
633            x = &x + &one;
634        }
635        x.eval().unwrap();
636        drop(one);
637        assert_eq!(
638            live::count() - before,
639            1,
640            "only the result's buffer is alive"
641        );
642        assert_eq!(x.item::<i64>().unwrap(), steps as i64);
643    }
644
645    #[test]
646    fn multi_output_once() {
647        let a = Array::from_slice(&[2i32, 3], &[2]).unwrap();
648        let b = Array::from_slice(&[5i32, 7], &[2]).unwrap();
649        let mut outs = Array::from_op(Primitive::AddMultiply, vec![a, b]).unwrap();
650        let product = outs.pop().unwrap();
651        let sum = outs.pop().unwrap();
652        counter::take();
653        eval(&[sum.clone(), product.clone()]).unwrap();
654        assert_eq!(counter::take(), 1);
655        drop(sum);
656        assert_eq!(product.to_vec::<i32>().unwrap(), vec![10, 21]);
657    }
658
659    #[test]
660    fn slice_update_writes_a_strided_region() {
661        // Base: a transposed view; region: rows 0..3 step 2, columns reversed.
662        let base = crate::arange(0.0, 12.0, 1.0, crate::DType::I32)
663            .unwrap()
664            .reshape(&[4, 3])
665            .unwrap()
666            .t()
667            .unwrap(); // [[0,3,6,9],[1,4,7,10],[2,5,8,11]]
668        let update = Array::from_slice(&[-1i32, -2, -3, -4, -5, -6, -7, -8], &[2, 4]).unwrap();
669        let primitive = Primitive::SliceUpdate {
670            start: vec![0, 3],
671            step: vec![2, -1],
672            len: vec![2, 4],
673        };
674        let out = Array::from_op(primitive.clone(), vec![base.clone(), update.clone()])
675            .unwrap()
676            .remove(0);
677        assert_eq!(
678            out.to_vec::<i32>().unwrap(),
679            [-4, -3, -2, -1, 1, 4, 7, 10, -8, -7, -6, -5]
680        );
681        let wrong = Array::from_slice(&[0i32; 3], &[3]).unwrap();
682        assert!(Array::from_op(primitive, vec![base, wrong]).is_err());
683    }
684
685    #[test]
686    fn tracer_nodes_keep_their_graph_only_while_tracing() {
687        let x = Array::scalar(2.0f32);
688        let kept = std::cell::Cell::new(false);
689        let g = crate::grad(|x: &Array| {
690            let y = x * x;
691            y.eval()?;
692            kept.set(matches!(&*y.node.lock(), State::Retained { .. }));
693            Ok(y)
694        });
695        assert_eq!(g(&x).unwrap().item::<f32>().unwrap(), 4.0);
696        assert!(kept.get(), "retained inside a transform");
697        let traced = crate::vjp(
698            |xs| Ok(vec![&xs[0] * &xs[0]]),
699            &[x],
700            &[Array::scalar(1.0f32)],
701        )
702        .unwrap()
703        .0
704        .remove(0);
705        traced.eval().unwrap();
706        assert!(
707            matches!(&*traced.node.lock(), State::Done),
708            "outside a transform"
709        );
710    }
711
712    #[test]
713    fn evaluation_detaches_inputs() {
714        let a = Array::scalar(1u8);
715        let c = &a + &a;
716        assert!(c.primitive().is_some());
717        c.eval().unwrap();
718        assert_eq!(c.primitive(), None);
719        assert_eq!(Arc::strong_count(&a.node), 1, "c no longer references a");
720    }
721
722    /// Positions of nodes in the evaluation order of `arrays`, checked to be topological.
723    fn positions(arrays: &[Array]) -> NodeMap<*const Node, usize> {
724        let order = topological_order(arrays);
725        let at: NodeMap<*const Node, usize> = order
726            .iter()
727            .enumerate()
728            .map(|(i, n)| (Arc::as_ptr(n), i))
729            .collect();
730        assert_eq!(at.len(), order.len(), "each node once");
731        for (i, node) in order.iter().enumerate() {
732            if let State::Pending { inputs, .. } = &*node.lock() {
733                for input in inputs {
734                    if let Some(&j) = at.get(&Arc::as_ptr(&input.node)) {
735                        assert!(j < i, "inputs come first");
736                    }
737                }
738            }
739        }
740        at
741    }
742
743    #[test]
744    fn independent_work_is_ordered_side_by_side() {
745        let x = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
746        let (a1, b1) = (&x + 1.0, &x + 3.0);
747        let (a2, b2) = (&a1 * 2.0, &b1 * 4.0);
748        let top = &a2 + &b2;
749        let at = positions(std::slice::from_ref(&top));
750        let p = |a: &Array| at[&Arc::as_ptr(&a.node)];
751        assert!(
752            p(&a1).max(p(&b1)) < p(&a2).min(p(&b2)),
753            "both chains advance together"
754        );
755        assert_eq!(top.to_vec::<f32>().unwrap(), [20.0, 26.0]);
756    }
757
758    #[test]
759    fn wide_and_shared_graphs_are_ordered() {
760        let x = Array::from_slice(&[1i32], &[1]).unwrap();
761        let one = Array::from_slice(&[1i32], &[1]).unwrap();
762        let shared = &x + &x;
763        // 100 chains of different depths (wider than the breadth-first limit), joined.
764        let mut joined = shared.clone();
765        for i in 0..100 {
766            let mut c = &shared + &Array::from_slice(&[i], &[1]).unwrap();
767            for _ in 0..i % 7 {
768                c = &c * &one;
769            }
770            joined = &joined + &c;
771        }
772        positions(&[joined.clone(), shared.clone(), joined.clone()]);
773        counter::take();
774        eval(&[joined.clone(), shared.clone()]).unwrap();
775        let nodes = 1 + 100 * 2 + (0..100).map(|i| i % 7).sum::<usize>();
776        assert_eq!(counter::take(), nodes, "every node once");
777        assert_eq!(
778            joined.item::<i32>().unwrap(),
779            2 + 100 * 2 + (0..100).sum::<i32>()
780        );
781    }
782
783    #[test]
784    fn scheduled_evaluation_skips_work_done_meanwhile() {
785        let x = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
786        let shared = &x * &x;
787        let top = &shared + &x;
788        let scheduled = schedule(std::slice::from_ref(&top));
789        assert_eq!(scheduled.arrays().len(), 1);
790        counter::take();
791        shared.eval().unwrap();
792        scheduled.eval().unwrap();
793        assert_eq!(counter::take(), 2, "shared once, then top");
794        assert_eq!(top.to_vec::<f32>().unwrap(), [2.0, 6.0]);
795    }
796}