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