Skip to main content

nuvec_core/
eval.rs

1//! Graph evaluation.
2
3use std::sync::Arc;
4
5use crate::array::{Completion, Data, Node, NodeMap, NodeSet, State};
6use crate::gpu::GpuEval;
7use crate::kernels::{self, Operand};
8use crate::primitive::Primitive;
9use crate::tree::{Leaves, collect_leaves};
10use crate::{Array, DeviceKind, Error, Result, view};
11
12/// Evaluates the leaves of `tree` and everything they depend on, and waits for them. Each
13/// node runs at most once, even across threads.
14///
15/// `tree` is any [`Leaves`] value: a slice or array of arrays (`eval(&[a, b])`), a `Vec`, a
16/// model, or a borrowed view such as `&(&loss, &grads)`, which evaluates both without
17/// cloning either.
18///
19/// Nodes on the GPU stream run as Metal kernels where one exists, and on the CPU (over the
20/// same unified memory) otherwise. Returns the first error among `arrays`. Errors are
21/// sticky: a failed array, and anything computed from it, fails with the same error on
22/// every later evaluation or readback.
23///
24/// A failed GPU command buffer fails the evaluation that encoded it (its outputs and those
25/// of its later command buffers) and every evaluation that read its results while they
26/// were in flight; independent evaluations, earlier or later, are unaffected.
27///
28/// Several threads may evaluate arrays that share work: each node runs once, and a thread
29/// that needs a node another thread is running waits for it (after submitting its own GPU
30/// work), receiving its error if it fails.
31pub fn eval<T: Leaves + ?Sized>(tree: &T) -> Result<()> {
32    schedule(&collect_leaves(tree)).eval()
33}
34
35/// Starts evaluating the leaves of `tree` (any [`Leaves`] value, as [`eval`]) and returns
36/// without waiting for GPU work: reading the arrays
37/// (or evaluating them) later waits for exactly the work they need. As in MLX, use it to
38/// overlap building the next computation with the GPU running this one.
39///
40/// CPU work runs on the calling thread before this returns. Errors are sticky: an error
41/// already known is returned; a GPU error is reported by the first read or evaluation of
42/// an affected array. Submission blocks while too many command buffers are in flight.
43///
44/// ```
45/// use nuvec_core::{Result, array, async_eval};
46///
47/// # fn main() -> Result<()> {
48/// let x = array![1.0, 2.0];
49/// let y = &x * 3.0;
50/// async_eval(&y)?;
51/// // ... build the next computation here ...
52/// assert_eq!(y.to_vec::<f32>()?, [3.0, 6.0]); // waits for `y` only
53/// # Ok(())
54/// # }
55/// ```
56pub fn async_eval<T: Leaves + ?Sized>(tree: &T) -> Result<()> {
57    schedule(&collect_leaves(tree)).async_eval()
58}
59
60/// `arrays` with the work they need put in order, to be started later by
61/// [`Scheduled::async_eval`] or [`Scheduled::eval`] with less delay: ordering a large
62/// graph takes a while, which can then overlap other work, such as the GPU running the
63/// previous step while the next one waits for its result.
64///
65/// The arrays are evaluated as [`async_eval`] or [`eval`] would at the time they are
66/// started; nodes evaluated in between (by another evaluation) are skipped.
67///
68/// ```
69/// use nuvec_core::{Result, array, schedule};
70///
71/// # fn main() -> Result<()> {
72/// let x = array![1.0, 2.0];
73/// let y = &(&x * 3.0) + 1.0;
74/// let scheduled = schedule(std::slice::from_ref(&y));
75/// // ... wait for something else here ...
76/// scheduled.async_eval()?;
77/// assert_eq!(y.to_vec::<f32>()?, [4.0, 7.0]);
78/// # Ok(())
79/// # }
80/// ```
81pub fn schedule(arrays: &[Array]) -> Scheduled {
82    Scheduled {
83        arrays: arrays.to_vec(),
84        order: topological_order(arrays),
85    }
86}
87
88/// Arrays whose evaluation is put in order but not started ([`schedule`]).
89pub struct Scheduled {
90    arrays: Vec<Array>,
91    order: Vec<Arc<Node>>,
92}
93
94impl std::fmt::Debug for Scheduled {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        f.debug_struct("Scheduled")
97            .field("arrays", &self.arrays)
98            .field("nodes", &self.order.len())
99            .finish()
100    }
101}
102
103impl Scheduled {
104    /// The scheduled arrays.
105    pub fn arrays(&self) -> &[Array] {
106        &self.arrays
107    }
108
109    /// Starts evaluating the arrays without waiting for GPU work, as [`async_eval`].
110    pub fn async_eval(self) -> Result<()> {
111        let arrays = self.arrays.clone();
112        self.submit().commit();
113        match arrays.iter().find_map(Array::slot_error) {
114            Some(err) => Err(err),
115            None => Ok(()),
116        }
117    }
118
119    /// Evaluates the arrays and waits for them, as [`eval`].
120    pub fn eval(self) -> Result<()> {
121        let arrays = self.arrays.clone();
122        // Errors are reported through the arrays' slots below.
123        let _ = self.submit().flush();
124        wait(&arrays)
125    }
126
127    /// Encodes or computes every node in order; the caller commits (or flushes) the GPU
128    /// work.
129    fn submit(self) -> GpuEval {
130        let mut gpu = GpuEval::new();
131        for node in self.order {
132            evaluate(&node, &mut gpu);
133            // `node` is released here, so intermediates are freed as soon as their last
134            // consumer has run (or, on the GPU, once their command buffer completes).
135        }
136        gpu
137    }
138}
139
140/// Waits for `arrays`; the first error among them.
141fn wait(arrays: &[Array]) -> Result<()> {
142    for array in arrays {
143        if let Err(err) = array.out().slot.wait() {
144            return Err(err.clone());
145        }
146    }
147    Ok(())
148}
149
150/// The widest breadth-first frontier when ordering a graph (MLX's `MLX_BFS_MAX_WIDTH`).
151const BFS_MAX_WIDTH: usize = 20;
152
153/// Unevaluated nodes reachable from `arrays`, inputs before consumers. Iterative, so graph
154/// depth is limited only by memory.
155///
156/// As MLX's `eval_impl`: a tape is built breadth-first back from the outputs, a node
157/// joining once all its consumers have, and run in reverse. Independent work (the query,
158/// key and value projections of a layer, say) is then encoded side by side, so the GPU
159/// runs it concurrently without barriers between. Past [`BFS_MAX_WIDTH`] pending nodes the
160/// walk goes depth-first, which bounds how many intermediates are alive at once.
161fn topological_order(arrays: &[Array]) -> Vec<Arc<Node>> {
162    // Unevaluated nodes with their unevaluated inputs, depth-first.
163    let mut inputs: NodeMap<*const Node, Vec<Arc<Node>>> = NodeMap::default();
164    let mut done = NodeSet::default();
165    let mut stack: Vec<Arc<Node>> = arrays.iter().map(|a| a.node.clone()).collect();
166    while let Some(node) = stack.pop() {
167        let key = Arc::as_ptr(&node);
168        if inputs.contains_key(&key) || done.contains(&key) {
169            continue;
170        }
171        let pending: Vec<Arc<Node>> = match &*node.lock() {
172            State::Pending { inputs, .. } => inputs.iter().map(|a| a.node.clone()).collect(),
173            // Running on another thread: its consumers wait for it (`input_data`).
174            State::Running { .. } | State::Retained { .. } | State::Done | State::Failed { .. } => {
175                done.insert(key);
176                continue;
177            }
178        };
179        stack.extend(pending.iter().cloned());
180        inputs.insert(key, pending);
181    }
182    for list in inputs.values_mut() {
183        list.retain(|n| !done.contains(&Arc::as_ptr(n)));
184    }
185    let roots: Vec<Arc<Node>> = arrays
186        .iter()
187        .map(|a| a.node.clone())
188        .filter(|n| inputs.contains_key(&Arc::as_ptr(n)))
189        .collect();
190    // Consumers of each node (an input used twice counts twice; the outputs count once
191    // each for the caller).
192    let mut consumers: NodeMap<*const Node, usize> = NodeMap::default();
193    for n in inputs.values().flatten().chain(&roots) {
194        *consumers.entry(Arc::as_ptr(n)).or_default() += 1;
195    }
196    // The tape: `None` stands for the caller, whose inputs are `roots`.
197    let mut tape: Vec<Option<Arc<Node>>> = vec![None];
198    let mut deferred: Vec<(Option<Arc<Node>>, usize)> = Vec::new();
199    let mut next = 0;
200    while !consumers.is_empty() && (next < tape.len() || !deferred.is_empty()) {
201        let (node, mut j) = if next < tape.len() {
202            next += 1;
203            (tape[next - 1].clone(), 0)
204        } else {
205            deferred.pop().expect("checked above")
206        };
207        let list = match &node {
208            Some(n) => &inputs[&Arc::as_ptr(n)],
209            None => &roots,
210        };
211        while j < list.len() {
212            let key = Arc::as_ptr(&list[j]);
213            if consumers.contains_key(&key) {
214                if tape.len() - next >= BFS_MAX_WIDTH {
215                    deferred.push((node.clone(), j));
216                    break;
217                }
218                let left = consumers.get_mut(&key).expect("checked above");
219                *left -= 1;
220                if *left == 0 {
221                    consumers.remove(&key);
222                    tape.push(Some(list[j].clone()));
223                }
224            }
225            j += 1;
226        }
227    }
228    tape.into_iter().rev().flatten().collect()
229}
230
231/// Runs one node whose inputs are all evaluated, stores (or, for GPU work, defers) its
232/// outputs, and detaches it (unless a running transform needs its graph).
233///
234/// The node's lock is held only to move it from `Pending` to `Running` and from `Running`
235/// to its final state: computing (which may wait for inputs another thread is producing)
236/// happens without it, so two evaluations sharing nodes never wait on each other's locks.
237/// A node another thread is running is skipped here; its consumers wait for it in
238/// [`input_data`].
239fn evaluate(node: &Arc<Node>, gpu: &mut GpuEval) {
240    let done = Arc::new(Completion::default());
241    let (primitive, mut inputs) = {
242        let mut state = node.lock();
243        if !matches!(*state, State::Pending { .. }) {
244            return; // evaluated, or running, on another thread
245        }
246        let running = State::Running {
247            owner: std::thread::current().id(),
248            done: done.clone(),
249        };
250        match std::mem::replace(&mut *state, running) {
251            State::Pending { primitive, inputs } => (primitive, inputs),
252            _ => unreachable!("checked above"),
253        }
254    };
255    let running = Running {
256        node,
257        done,
258        finished: false,
259    };
260    #[cfg(test)]
261    counter::inc();
262    let result = match donation(node, &primitive, &mut inputs, &mut *gpu) {
263        Some(base) => in_place(node, &primitive, &inputs, base, gpu),
264        None => compute(node, &primitive, &inputs, gpu),
265    };
266    let failure = match result {
267        Ok((outputs, true)) => {
268            gpu.defer(node.clone(), outputs);
269            None
270        }
271        Ok((outputs, false)) => {
272            for (output, data) in node.outputs.iter().zip(outputs) {
273                let _ = output.slot.set(Ok(data));
274            }
275            None
276        }
277        Err(err) => {
278            for output in &node.outputs {
279                let _ = output.slot.set(Err(err.clone()));
280            }
281            Some(err)
282        }
283    };
284    // A tracer keeps its graph while its transform runs, which releases it on exit.
285    if crate::transforms::retain(node) {
286        running.finish(State::Retained { primitive, inputs });
287        return;
288    }
289    running.finish(match failure {
290        Some(error) => State::Failed { error },
291        None => State::Done,
292    });
293    drop(inputs); // detach: release the inputs outside the lock
294}
295
296/// A node this thread is running. Finishing it publishes its final state and wakes the
297/// threads waiting for it; dropping it unfinished (a panic while computing) fails it, so
298/// waiters never hang.
299struct Running<'a> {
300    node: &'a Arc<Node>,
301    done: Arc<Completion>,
302    finished: bool,
303}
304
305impl Running<'_> {
306    fn finish(mut self, state: State) {
307        *self.node.lock() = state;
308        self.done.finish();
309        // Dropping `self` now releases `done` without failing the node.
310        self.finished = true;
311    }
312}
313
314impl Drop for Running<'_> {
315    fn drop(&mut self) {
316        if self.finished {
317            return;
318        }
319        let error = Error::Backend("the evaluation of this array panicked".into());
320        for output in &self.node.outputs {
321            let _ = output.slot.set(Err(error.clone()));
322        }
323        *self.node.lock() = State::Failed { error };
324        self.done.finish();
325    }
326}
327
328/// Waits until `node`, if another thread is running it, has finished. The owner may wait
329/// for GPU work of ours in turn, so our open command buffer is committed first. Returns
330/// the owner's error if the node failed.
331fn wait_running(node: &Node, gpu: &mut GpuEval) -> Result<()> {
332    let done = match &*node.lock() {
333        State::Running { owner, done } => {
334            if *owner == std::thread::current().id() {
335                return Err(Error::InvalidArgument(
336                    "eval: an array was evaluated from within its own evaluation".into(),
337                ));
338            }
339            done.clone()
340        }
341        State::Failed { error } => return Err(error.clone()),
342        _ => return Ok(()),
343    };
344    gpu.commit();
345    done.wait();
346    match &*node.lock() {
347        State::Failed { error } => Err(error.clone()),
348        _ => Ok(()),
349    }
350}
351
352/// An input to be written in place (MLX's donation), and its buffer: an update's base
353/// (`slice_update`, `norm_rope` or `paged_write` into a cache), or the input of an
354/// element-wise operation or softmax whose buffer can take the output (same dtype, shape
355/// and contiguous layout), as MLX's element-wise kernels reuse their inputs.
356fn donation(
357    node: &Node,
358    primitive: &Primitive,
359    inputs: &mut [Array],
360    gpu: &mut GpuEval,
361) -> Option<(usize, Data)> {
362    let device = node.stream.device;
363    if node.is_tracer() || device.kind == DeviceKind::Cpu || !gpu.available(device) {
364        return None;
365    }
366    let base = match primitive {
367        Primitive::SliceUpdate { .. }
368        | Primitive::Fast(crate::fast::imp::FastOp::NormRope { cache: true, .. })
369        | Primitive::Fast(crate::fast::imp::FastOp::PagedWrite) => Some(0),
370        Primitive::Fast(crate::fast::imp::FastOp::GatedDeltaSlots) => Some(5),
371        _ => None,
372    };
373    if let Some(at) = base {
374        if !donatable_size(inputs[at].size()) {
375            return None;
376        }
377        return owned(&mut inputs[at], gpu).map(|data| (at, data));
378    }
379    let [out] = node.outputs.as_slice() else {
380        return None;
381    };
382    for i in in_place_inputs(primitive, inputs, out) {
383        if let Some(data) = owned(&mut inputs[i], gpu) {
384            return Some((i, data));
385        }
386    }
387    None
388}
389
390/// Whether a buffer of `size` elements may be written in place: the in-place kernels index
391/// with 32 bits (as the GPU's `supports` requires of every operand).
392fn donatable_size(size: usize) -> bool {
393    size > 0 && size < u32::MAX as usize
394}
395
396/// The inputs of an element-wise operation or softmax whose buffer the output may take:
397/// the output's dtype (float, where every such kernel exists on the GPU) and shape.
398fn in_place_inputs(
399    primitive: &Primitive,
400    inputs: &[Array],
401    out: &crate::array::Output,
402) -> Vec<usize> {
403    use crate::{DType, UnaryOp};
404    let candidates: &[usize] = match primitive {
405        Primitive::Unary(op) if *op != UnaryOp::Conjugate => &[0],
406        Primitive::Binary(_) => &[0, 1],
407        Primitive::Select => &[1, 2],
408        Primitive::Fast(crate::fast::imp::FastOp::Softmax) => &[0],
409        _ => &[],
410    };
411    let float = matches!(out.dtype, DType::F16 | DType::BF16 | DType::F32);
412    if !float || !donatable_size(out.size) {
413        return Vec::new();
414    }
415    candidates
416        .iter()
417        .copied()
418        .filter(|&i| inputs[i].dtype() == out.dtype && inputs[i].dims() == out.shape.as_slice())
419        .collect()
420}
421
422/// The buffer of `input`, taken over to be written in place: only when the evaluation owns
423/// it outright (its node has no other strong handle, so no other array, view or consumer
424/// refers to it) and its data is the whole of a writable buffer with no other handle.
425/// Ownership proves the write exclusive, so no reference counting of other users is
426/// needed.
427///
428/// The input may be finished, still in flight in an earlier evaluation (pending work holds
429/// nodes and data only weakly, so the in-flight copy is then the data's only handle;
430/// taking it over under the lock that publishing takes means it is never published), or
431/// computed earlier in this evaluation. The buffer's fences and the encoder's barriers
432/// order the write after the work that produces and reads it.
433///
434/// Invariant (concurrency): the `Arc::strong_count` checks are race-free only because a
435/// node no other handle refers to is out of every graph: another thread can reach a node
436/// only through an `Arc` (an array, a consumer's inputs, or its own evaluation order),
437/// and each of those counts. Weak references from committed GPU work upgrade only to
438/// publish, under the in-flight lock that donation takes too. Arrays put in order by [`schedule`] hold their nodes strongly until started,
439/// so scheduled-but-unstarted work disables donation of what it reads.
440fn owned(input: &mut Array, gpu: &mut GpuEval) -> Option<Data> {
441    // Output `at` of the node (another output, if any, has no holder either: the node's
442    // only strong handle is this input).
443    let at = input.output as usize;
444    let base = &mut input.node;
445    let usable = |data: &Data, out: &crate::array::Output, handles: usize| {
446        out.flags.row_contiguous
447            && crate::gpu::writable(data, out)
448            && Arc::strong_count(&data.buffer) == handles
449    };
450    // Produced earlier in this evaluation: taken over from it if nothing else refers to it.
451    if let Some(out) = base.outputs.get(at)
452        && let Some(data) = gpu.take_pending(base, at, |data, handles| usable(data, out, handles))
453    {
454        return Some(data);
455    }
456    if Arc::strong_count(base) != 1 {
457        return None;
458    }
459    // Finished: no pending work refers to it any more, so it is owned outright.
460    if let Some(base) = Arc::get_mut(base) {
461        let out = base.outputs.get_mut(at)?;
462        if let Some(Ok(data)) = out.slot.get() {
463            // The slot and (if it was published from in-flight work) the in-flight copy.
464            let handles = 1 + usize::from(out.inflight.get().is_some());
465            if !usable(data, out, handles) {
466                return None;
467            }
468            out.inflight.clear();
469            return match out.slot.take() {
470                Some(Ok(data)) => Some(data),
471                _ => None,
472            };
473        }
474    }
475    // In flight: only weak references from pending work remain besides ours.
476    let out = base.outputs.get(at)?;
477    if out.slot.get().is_some() {
478        return None;
479    }
480    let mut state = out.inflight.lock();
481    let crate::array::InFlightState::Set(data) = &*state else {
482        return None;
483    };
484    if !usable(data, out, 1) {
485        return None;
486    }
487    match std::mem::replace(&mut *state, crate::array::InFlightState::Donated) {
488        crate::array::InFlightState::Set(data) => Some(data),
489        _ => None,
490    }
491}
492
493/// An update written into its donated base (input 0), or an element-wise operation or
494/// softmax written into its donated input `at`.
495fn in_place(
496    node: &Node,
497    primitive: &Primitive,
498    inputs: &[Array],
499    (at, base): (usize, Data),
500    gpu: &mut GpuEval,
501) -> Result<(Vec<Data>, bool)> {
502    let (mut pending, mut foreign) = (false, Vec::new());
503    let mut rest = Vec::with_capacity(inputs.len());
504    for (i, a) in inputs.iter().enumerate() {
505        rest.push(match i == at {
506            true => None,
507            false => Some(input_data(a, gpu, &mut pending, &mut foreign)?),
508        });
509    }
510    let out = &node.outputs[0];
511    let operands = |skip: usize| -> Vec<Operand<'_>> {
512        inputs
513            .iter()
514            .zip(&rest)
515            .skip(skip)
516            .filter_map(|(a, d)| d.as_ref().map(|d| (a.out(), d)))
517            .collect()
518    };
519    if let Primitive::Fast(crate::fast::imp::FastOp::GatedDeltaSlots) = primitive {
520        let pool = inputs[at].out();
521        let all = operands(0);
522        let datas = gpu.gated_delta_slots_in_place(&all, pool, base, &node.outputs)?;
523        return Ok((datas, true));
524    }
525    let data = match primitive {
526        Primitive::Fast(crate::fast::imp::FastOp::PagedWrite) => {
527            gpu.paged_write_in_place(base, &operands(1), out)?
528        }
529        Primitive::Fast(crate::fast::imp::FastOp::Softmax) => gpu.softmax_in_place(base, out)?,
530        Primitive::Fast(op) => gpu.norm_rope_in_place(base, op, &operands(1), out)?,
531        Primitive::SliceUpdate { .. } => {
532            let slices = primitive.dim_slices(out.shape.len())?;
533            gpu.slice_update_in_place(base, out, operands(1)[0], &slices)?
534        }
535        _ => {
536            let metas: Vec<&crate::array::Output> = inputs.iter().map(Array::out).collect();
537            let datas: Vec<Option<&Data>> = rest.iter().map(Option::as_ref).collect();
538            gpu.elementwise_in_place(primitive, &metas, &datas, base, out)?
539        }
540    };
541    Ok((vec![data], true))
542}
543
544/// An input's data: from this evaluation's GPU work, evaluated, or in flight in another
545/// evaluation (then `pending`, and recorded in `foreign`).
546fn input_data<'a>(
547    input: &'a Array,
548    gpu: &mut GpuEval,
549    pending: &mut bool,
550    foreign: &mut Vec<&'a Array>,
551) -> Result<Data> {
552    if let Some(d) = gpu.lookup(&input.node, input.output) {
553        *pending = true;
554        Ok(d)
555    } else if let Some(result) = input.out().slot.get() {
556        result.clone()
557    } else if let Some(d) = input.out().inflight.get() {
558        // Committed by another evaluation on the device timeline: GPU work encoded now
559        // runs after it, so it may read the data at once.
560        *pending = true;
561        foreign.push(input);
562        gpu.read_in_flight(input);
563        Ok(d)
564    } else {
565        // Produced by another evaluation (another thread, or an earlier `async_eval`):
566        // submit our own GPU work first, so two evaluations never wait on each other, then
567        // wait for the other thread to finish running the node.
568        gpu.commit();
569        wait_running(&input.node, gpu)?;
570        if let Some(result) = input.out().slot.get() {
571            return result.clone();
572        }
573        if let Some(d) = input.out().inflight.get() {
574            *pending = true;
575            foreign.push(input);
576            gpu.read_in_flight(input);
577            return Ok(d);
578        }
579        // Deferred in the producer's open GPU work, which it commits before it waits for
580        // anything and at the end of its evaluation.
581        input.out().slot.wait().clone()
582    }
583}
584
585/// Computes a node's outputs. The flag is true if they depend on GPU work that has not
586/// completed yet (and must be deferred until it has).
587fn compute(
588    node: &Node,
589    primitive: &Primitive,
590    inputs: &[Array],
591    gpu: &mut GpuEval,
592) -> Result<(Vec<Data>, bool)> {
593    let mut data = Vec::with_capacity(inputs.len());
594    let mut pending = false;
595    // Inputs read from another evaluation's in-flight GPU work.
596    let mut foreign = Vec::new();
597    for input in inputs {
598        data.push(input_data(input, gpu, &mut pending, &mut foreign)?);
599    }
600    let operands: Vec<Operand<'_>> = inputs
601        .iter()
602        .zip(&data)
603        .map(|(a, d)| (a.out(), d))
604        .collect();
605    // Waits until foreign in-flight inputs are complete (for work that does not run on the
606    // device timeline).
607    let await_foreign = || -> Result<()> {
608        for input in &foreign {
609            input.out().slot.wait().clone()?;
610        }
611        Ok(())
612    };
613    if matches!(primitive, Primitive::Depends { .. }) {
614        // An ordering point (blocked attention, checkpoints): the work before it is
615        // encoded, so the intermediates it no longer needs return to the allocator now,
616        // whatever their size, before the work after it allocates.
617        gpu.release_unreferenced();
618    }
619    if let Some(views) = view_outputs(primitive, &operands)? {
620        // Views do no work: they are published with this evaluation's GPU work (which runs
621        // after any foreign producer) or, without GPU work, once the producers complete.
622        gpu.depend_on(&foreign);
623        return Ok((views, pending));
624    }
625    let device = node.stream.device;
626    if device.kind != DeviceKind::Cpu {
627        if !gpu.available(device) {
628            return Err(Error::UnsupportedDevice(device));
629        }
630        if gpu.supports(primitive, &operands, &node.outputs) {
631            return Ok((gpu.run(primitive, &operands, &node.outputs)?, true));
632        }
633    }
634    // The CPU reads inputs in place: wait for the GPU work that produces them.
635    await_foreign()?;
636    if pending {
637        gpu.flush()?;
638    }
639    Ok((cpu(primitive, &operands)?, false))
640}
641
642/// Outputs of view primitives, which share their input's buffer (on every device), or
643/// `None` for primitives that compute.
644fn view_outputs(primitive: &Primitive, operands: &[Operand<'_>]) -> Result<Option<Vec<Data>>> {
645    let share = |offset| {
646        Some(vec![Data {
647            buffer: operands[0].1.buffer.clone(),
648            offset,
649        }])
650    };
651    Ok(match primitive {
652        Primitive::Transpose(_)
653        | Primitive::Broadcast(_)
654        | Primitive::StopGradient
655        | Primitive::Depends { .. } => share(operands[0].1.offset),
656        Primitive::Slice { .. } => {
657            let (meta, data) = operands[0];
658            let (_, delta) =
659                view::slice_geometry(&meta.strides, &primitive.dim_slices(meta.shape.len())?);
660            share(data.offset + delta)
661        }
662        Primitive::Custom(call) => Some(
663            operands[call.primals..]
664                .iter()
665                .map(|(_, data)| (*data).clone())
666                .collect(),
667        ),
668        Primitive::Reshape(shape) => {
669            let (meta, data) = operands[0];
670            view::nocopy_reshape(&meta.shape, &meta.strides, shape).and_then(|_| share(data.offset))
671        }
672        _ => None,
673    })
674}
675
676/// Computing primitives on the CPU.
677fn cpu(primitive: &Primitive, operands: &[Operand<'_>]) -> Result<Vec<Data>> {
678    let new = |buffer| Ok(vec![Data::new(buffer)]);
679    match primitive {
680        Primitive::Unary(op) => new(kernels::unary(*op, operands[0])?),
681        Primitive::Binary(op) => new(kernels::binary(*op, operands[0], operands[1])?),
682        Primitive::Select => new(kernels::select(operands[0], operands[1], operands[2])?),
683        Primitive::AsType(dtype) => new(kernels::cast(operands[0], *dtype)?),
684        Primitive::Reduce { op, axes } => new(kernels::reduce(*op, operands[0], axes)?),
685        Primitive::ArgReduce { op, axis } => new(kernels::arg_reduce(*op, operands[0], *axis)?),
686        Primitive::GatherAxis { axis } => {
687            new(kernels::gather_axis(operands[0], operands[1], *axis)?)
688        }
689        Primitive::ScatterAxis { axis, reduce } => new(kernels::scatter_axis(
690            operands[0],
691            operands[1],
692            operands[2],
693            *axis,
694            *reduce,
695        )?),
696        Primitive::Concatenate { axis } => {
697            let mut shape = operands[0].0.shape.clone();
698            shape[*axis] = operands.iter().map(|(m, _)| m.shape[*axis]).sum();
699            new(kernels::concatenate(operands, &shape, *axis)?)
700        }
701        Primitive::Sort { axis } => new(kernels::sort(operands[0], *axis, false)?),
702        Primitive::ArgSort { axis } => new(kernels::sort(operands[0], *axis, true)?),
703        Primitive::Scan {
704            op,
705            axis,
706            reverse,
707            inclusive,
708        } => new(kernels::scan(
709            *op,
710            operands[0],
711            *axis,
712            *reverse,
713            *inclusive,
714        )?),
715        Primitive::Matmul => new(kernels::matmul(operands[0], operands[1])?),
716        Primitive::AddMM { .. } => {
717            let (alpha, beta) = primitive.addmm_factors();
718            new(kernels::addmm(
719                operands[0],
720                operands[1],
721                operands[2],
722                alpha,
723                beta,
724            )?)
725        }
726        Primitive::RandomBits { shape, .. } => {
727            let n: usize = shape.iter().product();
728            new(kernels::random_bits(
729                operands[0],
730                n * primitive_width(primitive),
731            )?)
732        }
733        Primitive::Fast(op) => op.eval_cpu(operands),
734        Primitive::MetalKernel(_) => Err(Error::InvalidArgument(
735            "metal_kernel: custom kernels run on the GPU only".into(),
736        )),
737        Primitive::Compiled(fused) => fused.eval_cpu(operands),
738        Primitive::Quantize { group_size, bits } => {
739            let (packed, scales, biases) =
740                kernels::quantize(operands[0], *group_size, usize::from(*bits))?;
741            Ok(vec![
742                Data::new(packed),
743                Data::new(scales),
744                Data::new(biases),
745            ])
746        }
747        Primitive::Dequantize { group_size, bits } => new(kernels::dequantize(
748            operands[0],
749            operands[1],
750            operands[2],
751            *group_size,
752            usize::from(*bits),
753        )?),
754        Primitive::QuantizedMatmul {
755            group_size,
756            bits,
757            transpose,
758        } => new(kernels::quantized_matmul(
759            operands,
760            *group_size,
761            usize::from(*bits),
762            *transpose,
763        )?),
764        Primitive::SliceUpdate { .. } => new(kernels::slice_update(
765            operands[0],
766            operands[1],
767            &primitive.dim_slices(operands[0].0.shape.len())?,
768        )?),
769        // A reshape that `view_outputs` could not express as a view copies.
770        Primitive::Reshape(_) | Primitive::Copy => {
771            new(kernels::contiguous(operands[0].0, operands[0].1)?)
772        }
773        Primitive::Transpose(_)
774        | Primitive::Broadcast(_)
775        | Primitive::Slice { .. }
776        | Primitive::StopGradient
777        | Primitive::Depends { .. }
778        | Primitive::Custom(_) => {
779            unreachable!("views are handled by view_outputs")
780        }
781        #[cfg(test)]
782        Primitive::AddMultiply => {
783            use crate::primitive::BinaryOp;
784            Ok(vec![
785                Data::new(kernels::binary(BinaryOp::Add, operands[0], operands[1])?),
786                Data::new(kernels::binary(
787                    BinaryOp::Multiply,
788                    operands[0],
789                    operands[1],
790                )?),
791            ])
792        }
793    }
794}
795
796fn primitive_width(primitive: &Primitive) -> usize {
797    match primitive {
798        Primitive::RandomBits { width, .. } => usize::from(*width),
799        _ => unreachable!(),
800    }
801}
802
803/// Per-thread count of evaluated nodes, for run-once tests.
804#[cfg(test)]
805pub(crate) mod counter {
806    use std::cell::Cell;
807
808    thread_local!(static EVALUATED: Cell<usize> = const { Cell::new(0) });
809
810    pub(crate) fn inc() {
811        EVALUATED.set(EVALUATED.get() + 1);
812    }
813    pub(crate) fn take() -> usize {
814        EVALUATED.replace(0)
815    }
816}
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821    use crate::buffer::live;
822
823    #[test]
824    fn shared_node_runs_once() {
825        let x = Array::from_slice(&[1.0f32, 2.0], [2]);
826        let shared = &x + &x;
827        let left = &shared * &x;
828        let right = &shared + &x;
829        let top = &left + &right;
830        counter::take();
831        eval(&[top.clone(), left.clone()]).unwrap();
832        assert_eq!(counter::take(), 4, "shared, left, right, top: once each");
833        top.eval().unwrap();
834        assert_eq!(counter::take(), 0, "already evaluated");
835        assert_eq!(top.to_vec::<f32>().unwrap(), vec![5.0, 14.0]);
836    }
837
838    #[test]
839    fn running_releases_its_completion() {
840        // Both ends of a running node release the shared `Completion`: finishing it and
841        // dropping it unfinished (a panic while computing).
842        let x = Array::from_slice(&[1.0f32], [1]);
843        for finish in [true, false] {
844            let done = Arc::new(Completion::default());
845            let running = Running {
846                node: &x.node,
847                done: done.clone(),
848                finished: false,
849            };
850            if finish {
851                running.finish(State::Done);
852            } else {
853                drop(running);
854            }
855            assert_eq!(Arc::strong_count(&done), 1, "finish = {finish}");
856        }
857    }
858
859    #[test]
860    fn intermediates_freed() {
861        // The live count is per thread; GPU buffers are released on Metal's completion
862        // thread, so this checks the CPU path.
863        crate::set_default_stream(crate::Stream::default_for(crate::Device::cpu()));
864        let before = live::count();
865        let mut x = Array::from(0i64);
866        let one = Array::from(1i64);
867        let steps = if cfg!(miri) { 100 } else { 10_000 };
868        for _ in 0..steps {
869            x = &x + &one;
870        }
871        x.eval().unwrap();
872        drop(one);
873        assert_eq!(
874            live::count() - before,
875            1,
876            "only the result's buffer is alive"
877        );
878        assert_eq!(x.item::<i64>().unwrap(), steps as i64);
879    }
880
881    #[test]
882    fn multi_output_once() {
883        let a = Array::from_slice(&[2i32, 3], [2]);
884        let b = Array::from_slice(&[5i32, 7], [2]);
885        let mut outs = Array::from_op(Primitive::AddMultiply, vec![a, b]).unwrap();
886        let product = outs.pop().unwrap();
887        let sum = outs.pop().unwrap();
888        counter::take();
889        eval(&[sum.clone(), product.clone()]).unwrap();
890        assert_eq!(counter::take(), 1);
891        drop(sum);
892        assert_eq!(product.to_vec::<i32>().unwrap(), vec![10, 21]);
893    }
894
895    #[test]
896    fn slice_update_writes_a_strided_region() {
897        // Base: a transposed view; region: rows 0..3 step 2, columns reversed.
898        let base = crate::ops::imp::arange(0.0, 12.0, 1.0, crate::DType::I32)
899            .unwrap()
900            .try_reshape(&[4, 3])
901            .unwrap()
902            .t(); // [[0,3,6,9],[1,4,7,10],[2,5,8,11]]
903        let update = Array::from_slice(&[-1i32, -2, -3, -4, -5, -6, -7, -8], [2, 4]);
904        let primitive = Primitive::SliceUpdate {
905            start: vec![0, 3],
906            step: vec![2, -1],
907            len: vec![2, 4],
908        };
909        let out = Array::from_op(primitive.clone(), vec![base.clone(), update.clone()])
910            .unwrap()
911            .remove(0);
912        assert_eq!(
913            out.to_vec::<i32>().unwrap(),
914            [-4, -3, -2, -1, 1, 4, 7, 10, -8, -7, -6, -5]
915        );
916        let wrong = Array::from_slice(&[0i32; 3], [3]);
917        assert!(Array::from_op(primitive, vec![base, wrong]).is_err());
918    }
919
920    #[test]
921    fn tracer_nodes_keep_their_graph_only_while_tracing() {
922        let x = Array::from(2.0f32);
923        let kept = std::cell::Cell::new(false);
924        let g = crate::grad(|x: &Array| {
925            let y = x * x;
926            y.eval()?;
927            kept.set(matches!(&*y.node.lock(), State::Retained { .. }));
928            Ok(y)
929        });
930        assert_eq!(g(&x).unwrap().item::<f32>().unwrap(), 4.0);
931        assert!(kept.get(), "retained inside a transform");
932        let traced = crate::vjp_flat(|xs| Ok(vec![&xs[0] * &xs[0]]), &[x], &[Array::from(1.0f32)])
933            .unwrap()
934            .0
935            .remove(0);
936        traced.eval().unwrap();
937        assert!(
938            matches!(&*traced.node.lock(), State::Done),
939            "outside a transform"
940        );
941    }
942
943    #[test]
944    fn huge_buffers_are_never_donated() {
945        // The rule both donation paths (element-wise inputs and update bases) apply.
946        assert!(donatable_size(1));
947        assert!(donatable_size(u32::MAX as usize - 1));
948        assert!(!donatable_size(u32::MAX as usize));
949        assert!(!donatable_size(1 << 33));
950        assert!(!donatable_size(0));
951        // A lazy update of a base of 2^32 elements (metadata only) is never written in
952        // place. (A single dimension cannot exceed `i32::MAX`, so the base is 2-D.)
953        let base =
954            crate::ops::imp::broadcast_to(&Array::from(0.0f32), &[1 << 16, 1 << 16]).unwrap();
955        let update = Array::from_slice(&[1.0f32], [1, 1]);
956        let primitive = Primitive::SliceUpdate {
957            start: vec![0, 0],
958            step: vec![1, 1],
959            len: vec![1, 1],
960        };
961        let out = Array::from_op(primitive.clone(), vec![base.clone(), update.clone()])
962            .unwrap()
963            .remove(0);
964        let mut inputs = vec![base, update];
965        let mut gpu = GpuEval::new();
966        assert!(donation(&out.node, &primitive, &mut inputs, &mut gpu).is_none());
967    }
968
969    #[test]
970    fn evaluation_detaches_inputs() {
971        let a = Array::from(1u8);
972        let c = &a + &a;
973        assert!(c.primitive_t().is_some());
974        c.eval().unwrap();
975        assert_eq!(c.primitive_t(), None);
976        assert_eq!(Arc::strong_count(&a.node), 1, "c no longer references a");
977    }
978
979    /// Positions of nodes in the evaluation order of `arrays`, checked to be topological.
980    fn positions(arrays: &[Array]) -> NodeMap<*const Node, usize> {
981        let order = topological_order(arrays);
982        let at: NodeMap<*const Node, usize> = order
983            .iter()
984            .enumerate()
985            .map(|(i, n)| (Arc::as_ptr(n), i))
986            .collect();
987        assert_eq!(at.len(), order.len(), "each node once");
988        for (i, node) in order.iter().enumerate() {
989            if let State::Pending { inputs, .. } = &*node.lock() {
990                for input in inputs {
991                    if let Some(&j) = at.get(&Arc::as_ptr(&input.node)) {
992                        assert!(j < i, "inputs come first");
993                    }
994                }
995            }
996        }
997        at
998    }
999
1000    #[test]
1001    fn independent_work_is_ordered_side_by_side() {
1002        let x = Array::from_slice(&[1.0f32, 2.0], [2]);
1003        let (a1, b1) = (&x + 1.0, &x + 3.0);
1004        let (a2, b2) = (&a1 * 2.0, &b1 * 4.0);
1005        let top = &a2 + &b2;
1006        let at = positions(std::slice::from_ref(&top));
1007        let p = |a: &Array| at[&Arc::as_ptr(&a.node)];
1008        assert!(
1009            p(&a1).max(p(&b1)) < p(&a2).min(p(&b2)),
1010            "both chains advance together"
1011        );
1012        assert_eq!(top.to_vec::<f32>().unwrap(), [20.0, 26.0]);
1013    }
1014
1015    #[test]
1016    fn wide_and_shared_graphs_are_ordered() {
1017        let x = Array::from_slice(&[1i32], [1]);
1018        let one = Array::from_slice(&[1i32], [1]);
1019        let shared = &x + &x;
1020        // 100 chains of different depths (wider than the breadth-first limit), joined.
1021        let mut joined = shared.clone();
1022        for i in 0..100 {
1023            let mut c = &shared + &Array::from_slice(&[i], [1]);
1024            for _ in 0..i % 7 {
1025                c = &c * &one;
1026            }
1027            joined = &joined + &c;
1028        }
1029        positions(&[joined.clone(), shared.clone(), joined.clone()]);
1030        counter::take();
1031        eval(&[joined.clone(), shared.clone()]).unwrap();
1032        let nodes = 1 + 100 * 2 + (0..100).map(|i| i % 7).sum::<usize>();
1033        assert_eq!(counter::take(), nodes, "every node once");
1034        assert_eq!(
1035            joined.item::<i32>().unwrap(),
1036            2 + 100 * 2 + (0..100).sum::<i32>()
1037        );
1038    }
1039
1040    #[test]
1041    fn scheduled_evaluation_skips_work_done_meanwhile() {
1042        let x = Array::from_slice(&[1.0f32, 2.0], [2]);
1043        let shared = &x * &x;
1044        let top = &shared + &x;
1045        let scheduled = schedule(std::slice::from_ref(&top));
1046        assert_eq!(scheduled.arrays().len(), 1);
1047        counter::take();
1048        shared.eval().unwrap();
1049        scheduled.eval().unwrap();
1050        assert_eq!(counter::take(), 2, "shared once, then top");
1051        assert_eq!(top.to_vec::<f32>().unwrap(), [2.0, 6.0]);
1052    }
1053}