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() {
364        return None;
365    }
366    if device.kind == DeviceKind::Cpu {
367        // MLX's CPU donation for updates: the base's buffer, if nothing else holds it.
368        return match primitive {
369            Primitive::SliceUpdate { .. } if donatable_size(inputs[0].size()) => {
370                owned_on_cpu(&mut inputs[0]).map(|data| (0, data))
371            }
372            _ => None,
373        };
374    }
375    if !gpu.available(device) {
376        return None;
377    }
378    let base = match primitive {
379        Primitive::SliceUpdate { .. }
380        | Primitive::Fast(crate::fast::imp::FastOp::NormRope { cache: true, .. })
381        | Primitive::Fast(crate::fast::imp::FastOp::PagedWrite) => Some(0),
382        Primitive::Fast(crate::fast::imp::FastOp::GatedDeltaSlots) => Some(5),
383        _ => None,
384    };
385    if let Some(at) = base {
386        if !donatable_size(inputs[at].size()) {
387            return None;
388        }
389        return owned(&mut inputs[at], gpu).map(|data| (at, data));
390    }
391    let [out] = node.outputs.as_slice() else {
392        return None;
393    };
394    for i in in_place_inputs(primitive, inputs, out) {
395        if let Some(data) = owned(&mut inputs[i], gpu) {
396            return Some((i, data));
397        }
398    }
399    None
400}
401
402/// Whether an update base of `size` elements may be written in place: the update kernels
403/// index with 32 bits (as the GPU's `supports` requires of their operands).
404fn donatable_size(size: usize) -> bool {
405    size > 0 && size < u32::MAX as usize
406}
407
408/// The inputs of an element-wise operation or softmax whose buffer the output may take:
409/// the output's dtype (float, where every such kernel exists on the GPU) and shape.
410fn in_place_inputs(
411    primitive: &Primitive,
412    inputs: &[Array],
413    out: &crate::array::Output,
414) -> Vec<usize> {
415    use crate::{DType, UnaryOp};
416    let candidates: &[usize] = match primitive {
417        Primitive::Unary(op) if *op != UnaryOp::Conjugate => &[0],
418        Primitive::Binary(_) => &[0, 1],
419        Primitive::Select => &[1, 2],
420        Primitive::Fast(
421            crate::fast::imp::FastOp::Softmax | crate::fast::imp::FastOp::CausalSoftmax { .. },
422        ) => &[0],
423        _ => &[],
424    };
425    let float = matches!(out.dtype, DType::F16 | DType::BF16 | DType::F32);
426    if !float || out.size == 0 {
427        return Vec::new();
428    }
429    // From 2^32 elements, only layouts the GPU's large kernels address (as `supports`).
430    if out.size >= u32::MAX as usize {
431        let large = match primitive {
432            Primitive::Fast(_) => {
433                let axis = out.shape.last().copied().unwrap_or(1).max(1);
434                out.size / axis * 1024 < u32::MAX as usize
435            }
436            _ => {
437                let metas: Vec<&crate::array::Output> = inputs.iter().map(Array::out).collect();
438                crate::gpu::large_elementwise(&metas, &out.shape)
439            }
440        };
441        if !large {
442            return Vec::new();
443        }
444    }
445    candidates
446        .iter()
447        .copied()
448        .filter(|&i| inputs[i].dtype() == out.dtype && inputs[i].dims() == out.shape.as_slice())
449        .collect()
450}
451
452/// The buffer of `input`, taken over to be written in place: only when the evaluation owns
453/// it outright (its node has no other strong handle, so no other array, view or consumer
454/// refers to it) and its data is the whole of a writable buffer with no other handle.
455/// Ownership proves the write exclusive, so no reference counting of other users is
456/// needed.
457///
458/// The input may be finished, still in flight in an earlier evaluation (pending work holds
459/// nodes and data only weakly, so the in-flight copy is then the data's only handle;
460/// taking it over under the lock that publishing takes means it is never published), or
461/// computed earlier in this evaluation. The buffer's fences and the encoder's barriers
462/// order the write after the work that produces and reads it.
463///
464/// Invariant (concurrency): the `Arc::strong_count` checks are race-free only because a
465/// node no other handle refers to is out of every graph: another thread can reach a node
466/// only through an `Arc` (an array, a consumer's inputs, or its own evaluation order),
467/// and each of those counts. Weak references from committed GPU work upgrade only to
468/// publish, under the in-flight lock that donation takes too. Arrays put in order by
469/// [`schedule`] hold their nodes strongly until started, so scheduled-but-unstarted work
470/// disables donation of what it reads.
471fn owned(input: &mut Array, gpu: &mut GpuEval) -> Option<Data> {
472    // Output `at` of the node (another output, if any, has no holder either: the node's
473    // only strong handle is this input).
474    let at = input.output as usize;
475    let base = &mut input.node;
476    let usable = |data: &Data, out: &crate::array::Output, handles: usize| {
477        out.flags.row_contiguous
478            && crate::gpu::writable(data, out)
479            && Arc::strong_count(&data.buffer) == handles
480    };
481    // Produced earlier in this evaluation: taken over from it if nothing else refers to it.
482    if let Some(out) = base.outputs.get(at)
483        && let Some(data) = gpu.take_pending(base, at, |data, handles| usable(data, out, handles))
484    {
485        return Some(data);
486    }
487    // The in-flight cell is locked first: publishing locks it before it upgrades the node
488    // and drops the upgraded handle before it unlocks, so the count below is the node's real
489    // handles.
490    let cell = base.outputs.get(at)?.inflight.clone();
491    let mut state = cell.lock();
492    if Arc::strong_count(base) != 1 {
493        return None;
494    }
495    // Finished: no pending work refers to it any more, so it is owned outright.
496    if let Some(base) = Arc::get_mut(base) {
497        let out = base.outputs.get_mut(at)?;
498        if let Some(Ok(data)) = out.slot.get() {
499            // The slot and (if it was published from in-flight work) the in-flight copy.
500            let published = matches!(*state, crate::array::InFlightState::Set(_));
501            if !usable(data, out, 1 + usize::from(published)) {
502                return None;
503            }
504            if published {
505                *state = crate::array::InFlightState::Unset;
506            }
507            return match out.slot.take() {
508                Some(Ok(data)) => Some(data),
509                _ => None,
510            };
511        }
512    }
513    // In flight: only weak references from pending work remain besides ours.
514    let out = base.outputs.get(at)?;
515    if out.slot.get().is_some() {
516        return None;
517    }
518    let crate::array::InFlightState::Set(data) = &*state else {
519        return None;
520    };
521    if !usable(data, out, 1) {
522        return None;
523    }
524    match std::mem::replace(&mut *state, crate::array::InFlightState::Donated) {
525        crate::array::InFlightState::Set(data) => Some(data),
526        _ => None,
527    }
528}
529
530/// A finished input the CPU may write in place: no other handle to its node or buffer, the
531/// buffer holds exactly the row-major array, and it is CPU-writable (not a mapped file, no
532/// GPU work using it).
533fn owned_on_cpu(input: &mut Array) -> Option<Data> {
534    let at = input.output as usize;
535    let base = Arc::get_mut(&mut input.node)?;
536    let out = base.outputs.get_mut(at)?;
537    if out.inflight.get().is_some() {
538        return None;
539    }
540    let Some(Ok(data)) = out.slot.get() else {
541        return None;
542    };
543    let whole = data.offset == 0 && data.buffer.len() == out.size * out.dtype.size();
544    if !(out.flags.row_contiguous
545        && whole
546        && Arc::strong_count(&data.buffer) == 1
547        && data.buffer.cpu_writable())
548    {
549        return None;
550    }
551    match out.slot.take() {
552        Some(Ok(data)) => Some(data),
553        _ => None,
554    }
555}
556
557/// An update written into its donated base (input 0), or an element-wise operation or
558/// softmax written into its donated input `at`.
559fn in_place(
560    node: &Node,
561    primitive: &Primitive,
562    inputs: &[Array],
563    (at, base): (usize, Data),
564    gpu: &mut GpuEval,
565) -> Result<(Vec<Data>, bool)> {
566    let (mut pending, mut foreign) = (false, Vec::new());
567    let mut rest = Vec::with_capacity(inputs.len());
568    for (i, a) in inputs.iter().enumerate() {
569        rest.push(match i == at {
570            true => None,
571            false => Some(input_data(a, gpu, &mut pending, &mut foreign)?),
572        });
573    }
574    let out = &node.outputs[0];
575    let operands = |skip: usize| -> Vec<Operand<'_>> {
576        inputs
577            .iter()
578            .zip(&rest)
579            .skip(skip)
580            .filter_map(|(a, d)| d.as_ref().map(|d| (a.out(), d)))
581            .collect()
582    };
583    if let Primitive::Fast(crate::fast::imp::FastOp::GatedDeltaSlots) = primitive {
584        let pool = inputs[at].out();
585        let all = operands(0);
586        let datas = gpu.gated_delta_slots_in_place(&all, pool, base, &node.outputs)?;
587        return Ok((datas, true));
588    }
589    let data = match primitive {
590        Primitive::Fast(crate::fast::imp::FastOp::PagedWrite) => {
591            gpu.paged_write_in_place(base, &operands(1), out)?
592        }
593        Primitive::Fast(crate::fast::imp::FastOp::Softmax) => {
594            gpu.softmax_in_place(base, out, None)?
595        }
596        Primitive::Fast(crate::fast::imp::FastOp::CausalSoftmax { rows }) => {
597            gpu.softmax_in_place(base, out, Some(*rows as usize))?
598        }
599        Primitive::Fast(op) => gpu.norm_rope_in_place(base, op, &operands(1), out)?,
600        Primitive::SliceUpdate { .. } if node.stream.device.kind == DeviceKind::Cpu => {
601            // The CPU reads the update in place: wait for the GPU work that produces it.
602            for input in &foreign {
603                input.out().slot.wait().clone()?;
604            }
605            if pending {
606                gpu.flush()?;
607            }
608            let slices = primitive.dim_slices(out.shape.len())?;
609            let mut base = base;
610            let buffer = Arc::get_mut(&mut base.buffer).expect("donated: the only handle");
611            crate::kernels::slice_update_into(buffer, &out.shape, operands(1)[0], &slices);
612            return Ok((vec![base], false));
613        }
614        Primitive::SliceUpdate { .. } => {
615            let slices = primitive.dim_slices(out.shape.len())?;
616            gpu.slice_update_in_place(base, out, operands(1)[0], &slices)?
617        }
618        _ => {
619            let metas: Vec<&crate::array::Output> = inputs.iter().map(Array::out).collect();
620            let datas: Vec<Option<&Data>> = rest.iter().map(Option::as_ref).collect();
621            gpu.elementwise_in_place(primitive, &metas, &datas, base, out)?
622        }
623    };
624    Ok((vec![data], true))
625}
626
627/// An input's data: from this evaluation's GPU work, evaluated, or in flight in another
628/// evaluation (then `pending`, and recorded in `foreign`).
629fn input_data<'a>(
630    input: &'a Array,
631    gpu: &mut GpuEval,
632    pending: &mut bool,
633    foreign: &mut Vec<&'a Array>,
634) -> Result<Data> {
635    if let Some(d) = gpu.lookup(&input.node, input.output) {
636        *pending = true;
637        Ok(d)
638    } else if let Some(result) = input.out().slot.get() {
639        result.clone()
640    } else if let Some(d) = input.out().inflight.get() {
641        // Committed by another evaluation on the device timeline: GPU work encoded now
642        // runs after it, so it may read the data at once.
643        *pending = true;
644        foreign.push(input);
645        gpu.read_in_flight(input);
646        Ok(d)
647    } else {
648        // Produced by another evaluation (another thread, or an earlier `async_eval`):
649        // submit our own GPU work first, so two evaluations never wait on each other, then
650        // wait for the other thread to finish running the node.
651        gpu.commit();
652        wait_running(&input.node, gpu)?;
653        if let Some(result) = input.out().slot.get() {
654            return result.clone();
655        }
656        if let Some(d) = input.out().inflight.get() {
657            *pending = true;
658            foreign.push(input);
659            gpu.read_in_flight(input);
660            return Ok(d);
661        }
662        // Deferred in the producer's open GPU work, which it commits before it waits for
663        // anything and at the end of its evaluation.
664        input.out().slot.wait().clone()
665    }
666}
667
668/// Computes a node's outputs. The flag is true if they depend on GPU work that has not
669/// completed yet (and must be deferred until it has).
670fn compute(
671    node: &Node,
672    primitive: &Primitive,
673    inputs: &[Array],
674    gpu: &mut GpuEval,
675) -> Result<(Vec<Data>, bool)> {
676    let mut data = Vec::with_capacity(inputs.len());
677    let mut pending = false;
678    // Inputs read from another evaluation's in-flight GPU work.
679    let mut foreign = Vec::new();
680    for input in inputs {
681        data.push(input_data(input, gpu, &mut pending, &mut foreign)?);
682    }
683    let operands: Vec<Operand<'_>> = inputs
684        .iter()
685        .zip(&data)
686        .map(|(a, d)| (a.out(), d))
687        .collect();
688    // Waits until foreign in-flight inputs are complete (for work that does not run on the
689    // device timeline).
690    let await_foreign = || -> Result<()> {
691        for input in &foreign {
692            input.out().slot.wait().clone()?;
693        }
694        Ok(())
695    };
696    if matches!(primitive, Primitive::Depends { .. }) {
697        // An ordering point (blocked attention, checkpoints): the work before it is
698        // encoded, so the intermediates it no longer needs return to the allocator now,
699        // whatever their size, before the work after it allocates.
700        gpu.release_unreferenced();
701    }
702    if let Some(views) = view_outputs(primitive, &operands)? {
703        // Views do no work: they are published with this evaluation's GPU work (which runs
704        // after any foreign producer) or, without GPU work, once the producers complete.
705        gpu.depend_on(&foreign);
706        return Ok((views, pending));
707    }
708    let device = node.stream.device;
709    if device.kind != DeviceKind::Cpu {
710        if !gpu.available(device) {
711            return Err(Error::UnsupportedDevice(device));
712        }
713        if gpu.supports(primitive, &operands, &node.outputs) {
714            return Ok((gpu.run(primitive, &operands, &node.outputs)?, true));
715        }
716    }
717    // The CPU reads inputs in place: wait for the GPU work that produces them.
718    await_foreign()?;
719    if pending {
720        gpu.flush()?;
721    }
722    Ok((cpu(primitive, &operands)?, false))
723}
724
725/// Outputs of view primitives, which share their input's buffer (on every device), or
726/// `None` for primitives that compute.
727fn view_outputs(primitive: &Primitive, operands: &[Operand<'_>]) -> Result<Option<Vec<Data>>> {
728    let share = |offset| {
729        Some(vec![Data {
730            buffer: operands[0].1.buffer.clone(),
731            offset,
732        }])
733    };
734    Ok(match primitive {
735        Primitive::Transpose(_)
736        | Primitive::Broadcast(_)
737        | Primitive::StopGradient
738        | Primitive::Depends { .. } => share(operands[0].1.offset),
739        Primitive::Slice { .. } => {
740            let (meta, data) = operands[0];
741            let (_, delta) =
742                view::slice_geometry(&meta.strides, &primitive.dim_slices(meta.shape.len())?);
743            share(data.offset + delta)
744        }
745        Primitive::Custom(call) => Some(
746            operands[call.primals..]
747                .iter()
748                .map(|(_, data)| (*data).clone())
749                .collect(),
750        ),
751        Primitive::Reshape(shape) => {
752            let (meta, data) = operands[0];
753            view::nocopy_reshape(&meta.shape, &meta.strides, shape).and_then(|_| share(data.offset))
754        }
755        // Removing or inserting size-1 dimensions is always a view.
756        Primitive::Squeeze(_) | Primitive::ExpandDims(_) => share(operands[0].1.offset),
757        // The offset counts elements: in the new dtype it must stay whole.
758        Primitive::View(dtype) => {
759            let (meta, data) = operands[0];
760            let (from, to) = (meta.dtype.size() as isize, dtype.size() as isize);
761            let bytes = data.offset * from;
762            let shared = from == to || (meta.flags.row_contiguous && bytes % to == 0);
763            shared.then(|| share(bytes / to)).flatten()
764        }
765        _ => None,
766    })
767}
768
769/// Computing primitives on the CPU.
770fn cpu(primitive: &Primitive, operands: &[Operand<'_>]) -> Result<Vec<Data>> {
771    let new = |buffer| Ok(vec![Data::new(buffer)]);
772    match primitive {
773        Primitive::Unary(op) => new(kernels::unary(*op, operands[0])?),
774        Primitive::Binary(op) => new(kernels::binary(*op, operands[0], operands[1])?),
775        Primitive::Select => new(kernels::select(operands[0], operands[1], operands[2])?),
776        Primitive::AsType(dtype) => new(kernels::cast(operands[0], *dtype)?),
777        Primitive::ToFp8 => new(kernels::to_fp8(operands[0])?),
778        Primitive::SortedOffsets { groups } => new(kernels::sorted_offsets(operands[0], *groups)?),
779        Primitive::FromFp8(dtype) => new(kernels::from_fp8(operands[0], *dtype)?),
780        Primitive::Reduce { op, axes } => new(kernels::reduce(*op, operands[0], axes)?),
781        Primitive::ArgReduce { op, axis } => new(kernels::arg_reduce(*op, operands[0], *axis)?),
782        Primitive::GatherAxis { axis } => {
783            new(kernels::gather_axis(operands[0], operands[1], *axis)?)
784        }
785        Primitive::ScatterAxis { axis, reduce } => new(kernels::scatter_axis(
786            operands[0],
787            operands[1],
788            operands[2],
789            *axis,
790            *reduce,
791        )?),
792        Primitive::Concatenate { axis } => {
793            let mut shape = operands[0].0.shape.clone();
794            shape[*axis] = operands.iter().map(|(m, _)| m.shape[*axis]).sum();
795            new(kernels::concatenate(operands, &shape, *axis)?)
796        }
797        Primitive::Sort { axis } => new(kernels::sort(operands[0], *axis, false)?),
798        Primitive::ArgSort { axis } => new(kernels::sort(operands[0], *axis, true)?),
799        Primitive::Scan {
800            op,
801            axis,
802            reverse,
803            inclusive,
804        } => new(kernels::scan(
805            *op,
806            operands[0],
807            *axis,
808            *reverse,
809            *inclusive,
810        )?),
811        Primitive::Matmul => new(kernels::matmul(operands[0], operands[1])?),
812        Primitive::AddMM { .. } => {
813            let (alpha, beta) = primitive.addmm_factors();
814            new(kernels::addmm(
815                operands[0],
816                operands[1],
817                operands[2],
818                alpha,
819                beta,
820            )?)
821        }
822        Primitive::RandomBits { shape, .. } => {
823            let n: usize = shape.iter().product();
824            new(kernels::random_bits(
825                operands[0],
826                n * primitive_width(primitive),
827            )?)
828        }
829        Primitive::Fast(op) => op.eval_cpu(operands),
830        Primitive::MetalKernel(_) => Err(Error::InvalidArgument(
831            "metal_kernel: custom kernels run on the GPU only".into(),
832        )),
833        Primitive::Compiled(fused) => fused.eval_cpu(operands),
834        Primitive::Collective { op, group } => crate::distributed::eval_cpu(op, *group, operands),
835        Primitive::Quantize { quant } => {
836            let (group_size, bits) = quant.checked("quantize")?;
837            if quant.mode != crate::QuantMode::Affine {
838                let (packed, scales) = kernels::fp_quantize(operands[0], group_size, bits)?;
839                return Ok(vec![Data::new(packed), Data::new(scales)]);
840            }
841            let (packed, scales, biases) = kernels::quantize(operands[0], group_size, bits)?;
842            Ok(vec![
843                Data::new(packed),
844                Data::new(scales),
845                Data::new(biases),
846            ])
847        }
848        Primitive::Dequantize { quant, dtype } => {
849            let (group_size, bits) = quant.checked("dequantize")?;
850            new(match quant.mode {
851                crate::QuantMode::Affine => {
852                    kernels::dequantize(operands[0], operands[1], operands[2], group_size, bits)?
853                }
854                _ => kernels::fp_dequantize(operands[0], operands[1], group_size, bits, *dtype)?,
855            })
856        }
857        Primitive::QuantizedMatmul { quant, transpose } => {
858            let (group_size, bits) = quant.checked("quantized_matmul")?;
859            new(kernels::quantized_matmul(
860                operands, *quant, group_size, bits, *transpose,
861            )?)
862        }
863        Primitive::SliceUpdate { .. } => new(kernels::slice_update(
864            operands[0],
865            operands[1],
866            &primitive.dim_slices(operands[0].0.shape.len())?,
867        )?),
868        // A reshape that `view_outputs` could not express as a view copies; so does a `view`,
869        // whose copy is then read in the new dtype.
870        Primitive::Reshape(_)
871        | Primitive::Squeeze(_)
872        | Primitive::ExpandDims(_)
873        | Primitive::Copy
874        | Primitive::View(_) => new(kernels::contiguous(operands[0].0, operands[0].1)?),
875        Primitive::Transpose(_)
876        | Primitive::Broadcast(_)
877        | Primitive::Slice { .. }
878        | Primitive::StopGradient
879        | Primitive::Depends { .. }
880        | Primitive::Custom(_) => {
881            unreachable!("views are handled by view_outputs")
882        }
883        #[cfg(test)]
884        Primitive::AddMultiply => {
885            use crate::primitive::BinaryOp;
886            Ok(vec![
887                Data::new(kernels::binary(BinaryOp::Add, operands[0], operands[1])?),
888                Data::new(kernels::binary(
889                    BinaryOp::Multiply,
890                    operands[0],
891                    operands[1],
892                )?),
893            ])
894        }
895    }
896}
897
898fn primitive_width(primitive: &Primitive) -> usize {
899    match primitive {
900        Primitive::RandomBits { width, .. } => usize::from(*width),
901        _ => unreachable!(),
902    }
903}
904
905/// Per-thread count of evaluated nodes, for run-once tests.
906#[cfg(test)]
907pub(crate) mod counter {
908    use std::cell::Cell;
909
910    thread_local!(static EVALUATED: Cell<usize> = const { Cell::new(0) });
911
912    pub(crate) fn inc() {
913        EVALUATED.set(EVALUATED.get() + 1);
914    }
915    pub(crate) fn take() -> usize {
916        EVALUATED.replace(0)
917    }
918}
919
920#[cfg(test)]
921mod tests {
922    use super::*;
923    use crate::buffer::live;
924
925    #[test]
926    fn shared_node_runs_once() {
927        let x = Array::from_slice(&[1.0f32, 2.0], [2]);
928        let shared = &x + &x;
929        let left = &shared * &x;
930        let right = &shared + &x;
931        let top = &left + &right;
932        counter::take();
933        eval(&[top.clone(), left.clone()]).unwrap();
934        assert_eq!(counter::take(), 4, "shared, left, right, top: once each");
935        top.eval().unwrap();
936        assert_eq!(counter::take(), 0, "already evaluated");
937        assert_eq!(top.to_vec::<f32>().unwrap(), vec![5.0, 14.0]);
938    }
939
940    #[test]
941    fn running_releases_its_completion() {
942        // Both ends of a running node release the shared `Completion`: finishing it and
943        // dropping it unfinished (a panic while computing).
944        let x = Array::from_slice(&[1.0f32], [1]);
945        for finish in [true, false] {
946            let done = Arc::new(Completion::default());
947            let running = Running {
948                node: &x.node,
949                done: done.clone(),
950                finished: false,
951            };
952            if finish {
953                running.finish(State::Done);
954            } else {
955                drop(running);
956            }
957            assert_eq!(Arc::strong_count(&done), 1, "finish = {finish}");
958        }
959    }
960
961    #[test]
962    fn intermediates_freed() {
963        // The live count is per thread; GPU buffers are released on Metal's completion
964        // thread, so this checks the CPU path.
965        crate::set_default_stream(crate::Stream::default_for(crate::Device::cpu()));
966        let before = live::count();
967        let mut x = Array::from(0i64);
968        let one = Array::from(1i64);
969        let steps = if cfg!(miri) { 100 } else { 10_000 };
970        for _ in 0..steps {
971            x = &x + &one;
972        }
973        x.eval().unwrap();
974        drop(one);
975        assert_eq!(
976            live::count() - before,
977            1,
978            "only the result's buffer is alive"
979        );
980        assert_eq!(x.item::<i64>().unwrap(), steps as i64);
981    }
982
983    #[test]
984    fn multi_output_once() {
985        let a = Array::from_slice(&[2i32, 3], [2]);
986        let b = Array::from_slice(&[5i32, 7], [2]);
987        let mut outs = Array::from_op(Primitive::AddMultiply, vec![a, b]).unwrap();
988        let product = outs.pop().unwrap();
989        let sum = outs.pop().unwrap();
990        counter::take();
991        eval(&[sum.clone(), product.clone()]).unwrap();
992        assert_eq!(counter::take(), 1);
993        drop(sum);
994        assert_eq!(product.to_vec::<i32>().unwrap(), vec![10, 21]);
995    }
996
997    #[test]
998    fn slice_update_writes_a_strided_region() {
999        // Base: a transposed view; region: rows 0..3 step 2, columns reversed.
1000        let base = crate::ops::imp::arange(0.0, 12.0, 1.0, crate::DType::I32)
1001            .unwrap()
1002            .try_reshape(&[4, 3])
1003            .unwrap()
1004            .t(); // [[0,3,6,9],[1,4,7,10],[2,5,8,11]]
1005        let update = Array::from_slice(&[-1i32, -2, -3, -4, -5, -6, -7, -8], [2, 4]);
1006        let primitive = Primitive::SliceUpdate {
1007            start: vec![0, 3],
1008            step: vec![2, -1],
1009            len: vec![2, 4],
1010        };
1011        let out = Array::from_op(primitive.clone(), vec![base.clone(), update.clone()])
1012            .unwrap()
1013            .remove(0);
1014        assert_eq!(
1015            out.to_vec::<i32>().unwrap(),
1016            [-4, -3, -2, -1, 1, 4, 7, 10, -8, -7, -6, -5]
1017        );
1018        let wrong = Array::from_slice(&[0i32; 3], [3]);
1019        assert!(Array::from_op(primitive, vec![base, wrong]).is_err());
1020    }
1021
1022    #[test]
1023    fn tracer_nodes_keep_their_graph_only_while_tracing() {
1024        let x = Array::from(2.0f32);
1025        let kept = std::cell::Cell::new(false);
1026        let g = crate::grad(|x: &Array| {
1027            let y = x * x;
1028            y.eval()?;
1029            kept.set(matches!(&*y.node.lock(), State::Retained { .. }));
1030            Ok(y)
1031        });
1032        assert_eq!(g(&x).unwrap().item::<f32>().unwrap(), 4.0);
1033        assert!(kept.get(), "retained inside a transform");
1034        let traced = crate::vjp_flat(|xs| Ok(vec![&xs[0] * &xs[0]]), &[x], &[Array::from(1.0f32)])
1035            .unwrap()
1036            .0
1037            .remove(0);
1038        traced.eval().unwrap();
1039        assert!(
1040            matches!(&*traced.node.lock(), State::Done),
1041            "outside a transform"
1042        );
1043    }
1044
1045    #[test]
1046    fn huge_buffers_are_never_donated() {
1047        // The rule both donation paths (element-wise inputs and update bases) apply.
1048        assert!(donatable_size(1));
1049        assert!(donatable_size(u32::MAX as usize - 1));
1050        assert!(!donatable_size(u32::MAX as usize));
1051        assert!(!donatable_size(1 << 33));
1052        assert!(!donatable_size(0));
1053        // A lazy update of a base of 2^32 elements (metadata only) is never written in
1054        // place. (A single dimension cannot exceed `i32::MAX`, so the base is 2-D.)
1055        let base =
1056            crate::ops::imp::broadcast_to(&Array::from(0.0f32), &[1 << 16, 1 << 16]).unwrap();
1057        let update = Array::from_slice(&[1.0f32], [1, 1]);
1058        let primitive = Primitive::SliceUpdate {
1059            start: vec![0, 0],
1060            step: vec![1, 1],
1061            len: vec![1, 1],
1062        };
1063        let out = Array::from_op(primitive.clone(), vec![base.clone(), update.clone()])
1064            .unwrap()
1065            .remove(0);
1066        let mut inputs = vec![base, update];
1067        let mut gpu = GpuEval::new();
1068        assert!(donation(&out.node, &primitive, &mut inputs, &mut gpu).is_none());
1069    }
1070
1071    #[test]
1072    fn evaluation_detaches_inputs() {
1073        let a = Array::from(1u8);
1074        let c = &a + &a;
1075        assert!(c.primitive_t().is_some());
1076        c.eval().unwrap();
1077        assert_eq!(c.primitive_t(), None);
1078        assert_eq!(Arc::strong_count(&a.node), 1, "c no longer references a");
1079    }
1080
1081    /// Positions of nodes in the evaluation order of `arrays`, checked to be topological.
1082    fn positions(arrays: &[Array]) -> NodeMap<*const Node, usize> {
1083        let order = topological_order(arrays);
1084        let at: NodeMap<*const Node, usize> = order
1085            .iter()
1086            .enumerate()
1087            .map(|(i, n)| (Arc::as_ptr(n), i))
1088            .collect();
1089        assert_eq!(at.len(), order.len(), "each node once");
1090        for (i, node) in order.iter().enumerate() {
1091            if let State::Pending { inputs, .. } = &*node.lock() {
1092                for input in inputs {
1093                    if let Some(&j) = at.get(&Arc::as_ptr(&input.node)) {
1094                        assert!(j < i, "inputs come first");
1095                    }
1096                }
1097            }
1098        }
1099        at
1100    }
1101
1102    #[test]
1103    fn independent_work_is_ordered_side_by_side() {
1104        let x = Array::from_slice(&[1.0f32, 2.0], [2]);
1105        let (a1, b1) = (&x + 1.0, &x + 3.0);
1106        let (a2, b2) = (&a1 * 2.0, &b1 * 4.0);
1107        let top = &a2 + &b2;
1108        let at = positions(std::slice::from_ref(&top));
1109        let p = |a: &Array| at[&Arc::as_ptr(&a.node)];
1110        assert!(
1111            p(&a1).max(p(&b1)) < p(&a2).min(p(&b2)),
1112            "both chains advance together"
1113        );
1114        assert_eq!(top.to_vec::<f32>().unwrap(), [20.0, 26.0]);
1115    }
1116
1117    #[test]
1118    fn wide_and_shared_graphs_are_ordered() {
1119        let x = Array::from_slice(&[1i32], [1]);
1120        let one = Array::from_slice(&[1i32], [1]);
1121        let shared = &x + &x;
1122        // 100 chains of different depths (wider than the breadth-first limit), joined.
1123        let mut joined = shared.clone();
1124        for i in 0..100 {
1125            let mut c = &shared + &Array::from_slice(&[i], [1]);
1126            for _ in 0..i % 7 {
1127                c = &c * &one;
1128            }
1129            joined = &joined + &c;
1130        }
1131        positions(&[joined.clone(), shared.clone(), joined.clone()]);
1132        counter::take();
1133        eval(&[joined.clone(), shared.clone()]).unwrap();
1134        let nodes = 1 + 100 * 2 + (0..100).map(|i| i % 7).sum::<usize>();
1135        assert_eq!(counter::take(), nodes, "every node once");
1136        assert_eq!(
1137            joined.item::<i32>().unwrap(),
1138            2 + 100 * 2 + (0..100).sum::<i32>()
1139        );
1140    }
1141
1142    #[test]
1143    fn scheduled_evaluation_skips_work_done_meanwhile() {
1144        let x = Array::from_slice(&[1.0f32, 2.0], [2]);
1145        let shared = &x * &x;
1146        let top = &shared + &x;
1147        let scheduled = schedule(std::slice::from_ref(&top));
1148        assert_eq!(scheduled.arrays().len(), 1);
1149        counter::take();
1150        shared.eval().unwrap();
1151        scheduled.eval().unwrap();
1152        assert_eq!(counter::take(), 2, "shared once, then top");
1153        assert_eq!(top.to_vec::<f32>().unwrap(), [2.0, 6.0]);
1154    }
1155}