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