Skip to main content

nuvec_core/
transforms.rs

1//! Function transforms: reverse-mode ([`vjp`], [`grad`], [`value_and_grad`]) and
2//! forward-mode ([`jvp`]) automatic differentiation.
3//!
4//! As in MLX, the function runs on *tracer* inputs and records a lazy graph. The derivative
5//! is another lazy graph, built from each primitive's rule, so transforms compose: the
6//! gradient of a gradient is a second derivative. Tracers are zero-copy identity views of
7//! the inputs. While a transform runs, evaluating a node that depends on a tracer keeps its
8//! graph instead of detaching it, so calling `eval` inside the function is allowed.
9
10use std::cell::RefCell;
11use std::collections::HashSet;
12use std::marker::PhantomData;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Weak};
15
16use crate::array::{Node, NodeMap, NodeSet, State};
17use crate::cast::Canon;
18use crate::derivatives::zeros_like;
19use crate::ops::imp as ops;
20use crate::primitive::Primitive;
21use crate::tree::{LeafKind, Leaves, Tree, Visit, Visitor, broadcast_zero, differentiable};
22use crate::{Array, Category, Error, Result};
23
24/// Identifies one run of a transform. Ids increase, and transforms nest like a stack, so
25/// the smallest live id among a node's inputs is the outermost transform it depends on.
26pub(crate) type TraceId = u64;
27
28/// The trace of `compile`'s placeholders: always live (evaluating them fails anyway).
29pub(crate) const PLACEHOLDER: TraceId = 0;
30
31static NEXT_TRACE: AtomicU64 = AtomicU64::new(PLACEHOLDER + 1);
32
33/// A running transform: its id and the nodes it retained (evaluated tracers that keep
34/// their graph), released when it exits.
35struct Frame {
36    id: TraceId,
37    retained: Vec<Weak<Node>>,
38}
39
40thread_local!(static LIVE: RefCell<Vec<Frame>> = const { RefCell::new(Vec::new()) });
41
42/// Whether transform `id` is running on this thread.
43pub(crate) fn is_live(id: TraceId) -> bool {
44    id == PLACEHOLDER || LIVE.with_borrow(|live| live.iter().any(|f| f.id == id))
45}
46
47/// The trace of a node recorded on `inputs`: the outermost live transform among theirs.
48pub(crate) fn inherited(inputs: &[Array]) -> Option<TraceId> {
49    inputs
50        .iter()
51        .filter_map(|a| a.node.trace)
52        .filter(|&id| is_live(id))
53        .min()
54}
55
56/// Registers `node`, just evaluated, to keep its graph until the transform it depends on
57/// exits; false if it is not a tracer (of a transform running on this thread).
58pub(crate) fn retain(node: &Arc<Node>) -> bool {
59    let Some(id) = node.trace.filter(|&id| id != PLACEHOLDER) else {
60        return false;
61    };
62    LIVE.with_borrow_mut(|live| match live.iter_mut().find(|f| f.id == id) {
63        Some(frame) => {
64            frame.retained.push(Arc::downgrade(node));
65            true
66        }
67        None => false,
68    })
69}
70
71/// Marks a transform as running on this thread while alive (nests). On exit, the nodes
72/// it retained detach (become plain evaluated nodes) and its arrays stop being tracers.
73struct Tracing {
74    id: TraceId,
75}
76
77impl Tracing {
78    fn enter() -> Self {
79        let id = NEXT_TRACE.fetch_add(1, Ordering::Relaxed);
80        LIVE.with_borrow_mut(|live| {
81            live.push(Frame {
82                id,
83                retained: Vec::new(),
84            });
85        });
86        Tracing { id }
87    }
88
89    /// A tracer for `primal`: an identity view (a broadcast to its own shape). It is
90    /// differentiable, so an enclosing transform differentiates through it.
91    fn tracer(&self, primal: &Array) -> Result<Array> {
92        let shape = primal.dims().to_vec();
93        let inputs = vec![primal.clone()];
94        let trace = Some(inherited(&inputs).map_or(self.id, |outer| outer.min(self.id)));
95        Ok(Array::record(Primitive::Broadcast(shape), inputs, trace)?.swap_remove(0))
96    }
97}
98
99impl Drop for Tracing {
100    fn drop(&mut self) {
101        let frame = LIVE.with_borrow_mut(|live| {
102            let at = live.iter().rposition(|f| f.id == self.id)?;
103            Some(live.remove(at))
104        });
105        for node in frame.into_iter().flat_map(|f| f.retained) {
106            let Some(node) = node.upgrade() else { continue };
107            let inputs = {
108                let mut state = node.lock();
109                match std::mem::replace(&mut *state, State::Done) {
110                    State::Retained { inputs, .. } => inputs,
111                    other => {
112                        *state = other;
113                        continue;
114                    }
115                }
116            };
117            drop(inputs); // detach outside the lock
118        }
119    }
120}
121
122type Key = (*const Node, u32);
123
124fn key(a: &Array) -> Key {
125    (Arc::as_ptr(&a.node), a.output)
126}
127
128/// A recorded node on a path from the tracers to the outputs.
129struct Step {
130    node: Arc<Node>,
131    primitive: Primitive,
132    inputs: Vec<Array>,
133    /// Which inputs depend on a tracer.
134    wanted: Vec<bool>,
135}
136
137impl Step {
138    fn outputs(&self) -> Vec<Array> {
139        (0..self.node.outputs.len() as u32)
140            .map(|output| Array {
141                node: self.node.clone(),
142                output,
143            })
144            .collect()
145    }
146}
147
148/// A node's primitive and inputs.
149type Graph = (Primitive, Vec<Array>);
150
151/// The primitive and inputs of a node whose graph is still known.
152fn graph(node: &Node) -> Option<Graph> {
153    match &*node.lock() {
154        State::Pending { primitive, inputs } | State::Retained { primitive, inputs } => {
155            Some((primitive.clone(), inputs.clone()))
156        }
157        State::Running { .. } | State::Done | State::Failed { .. } => None,
158    }
159}
160
161/// The inputs a transform follows: a custom function's own outputs (its trailing inputs)
162/// are handled by its rules, so its graph is never walked.
163fn followed(primitive: &Primitive, inputs: &[Array]) -> usize {
164    match primitive {
165        Primitive::Custom(call) => call.primals,
166        _ => inputs.len(),
167    }
168}
169
170/// Nodes between `sources` and `outputs` (those that depend on a source), inputs before
171/// consumers. For differentiation (`stop_gradient` true), `StopGradient` cuts the graph.
172/// Iterative, so depth is limited only by memory.
173fn tape(outputs: &[Array], sources: &NodeSet, stop_gradient: bool) -> Vec<Step> {
174    let mut depends: NodeMap<*const Node, bool> = NodeMap::default();
175    let mut visited = NodeSet::default();
176    let mut order = Vec::new();
177    let mut stack: Vec<(Arc<Node>, Option<Graph>)> =
178        outputs.iter().map(|a| (a.node.clone(), None)).collect();
179    while let Some((node, expanded)) = stack.pop() {
180        let ptr = Arc::as_ptr(&node);
181        if let Some((primitive, inputs)) = expanded {
182            let n = followed(&primitive, &inputs);
183            let wanted: Vec<bool> = inputs
184                .iter()
185                .enumerate()
186                .map(|(i, a)| i < n && depends.get(&Arc::as_ptr(&a.node)).copied().unwrap_or(false))
187                .collect();
188            let cut = stop_gradient && primitive == Primitive::StopGradient;
189            let dep = !cut && wanted.contains(&true);
190            depends.insert(ptr, dep);
191            if dep {
192                order.push(Step {
193                    node,
194                    primitive,
195                    inputs,
196                    wanted,
197                });
198            }
199            continue;
200        }
201        if !visited.insert(ptr) {
202            continue;
203        }
204        if sources.contains(&ptr) {
205            depends.insert(ptr, true);
206            continue;
207        }
208        let Some((primitive, inputs)) = graph(&node) else {
209            continue; // evaluated data: a constant
210        };
211        let next: Vec<Arc<Node>> = inputs[..followed(&primitive, &inputs)]
212            .iter()
213            .map(|a| a.node.clone())
214            .filter(|n| !visited.contains(&Arc::as_ptr(n)))
215            .collect();
216        stack.push((node, Some((primitive, inputs))));
217        stack.extend(next.into_iter().map(|n| (n, None)));
218    }
219    order
220}
221
222fn accumulate(map: &mut NodeMap<Key, Array>, key: Key, value: Array) -> Result<()> {
223    let value = match map.remove(&key) {
224        Some(previous) => ops::add(&previous, &value)?,
225        None => value,
226    };
227    map.insert(key, value);
228    Ok(())
229}
230
231fn check_shapes(op: &str, what: &str, arrays: &[Array], given: &[Array]) -> Result<()> {
232    if arrays.len() != given.len() {
233        return Err(Error::InvalidArgument(format!(
234            "{op}: {} {what}s for {} arrays",
235            given.len(),
236            arrays.len()
237        )));
238    }
239    for (a, g) in arrays.iter().zip(given) {
240        if a.dims() != g.dims() {
241            return Err(Error::InvalidArgument(format!(
242                "{op}: shape {:?} does not match {what} shape {:?}",
243                a.dims(),
244                g.dims()
245            )));
246        }
247        if a.dtype() != g.dtype() {
248            return Err(Error::InvalidArgument(format!(
249                "{op}: dtype {} does not match {what} dtype {}",
250                a.dtype(),
251                g.dtype()
252            )));
253        }
254    }
255    Ok(())
256}
257
258/// Reverse pass: the cotangents of `tracers`, given cotangents for `outputs`.
259fn reverse(outputs: &[Array], cotangents: &[Array], tracers: &[Array]) -> Result<Vec<Array>> {
260    let sources = tracers.iter().map(|t| Arc::as_ptr(&t.node)).collect();
261    let mut cot = NodeMap::default();
262    for (o, c) in outputs.iter().zip(cotangents) {
263        accumulate(&mut cot, key(o), c.clone())?;
264    }
265    for step in tape(outputs, &sources, true).iter().rev() {
266        let outs = step.outputs();
267        if !outs.iter().any(|o| cot.contains_key(&key(o))) {
268            continue;
269        }
270        let cots = outs
271            .iter()
272            .map(|o| cot.remove(&key(o)).map_or_else(|| zeros_like(o), Ok))
273            .collect::<Result<Vec<_>>>()?;
274        let grads = step
275            .primitive
276            .vjp(&step.inputs, &outs, &cots, &step.wanted)?;
277        for ((input, grad), &wanted) in step.inputs.iter().zip(grads).zip(&step.wanted) {
278            if let (Some(grad), true) = (grad, wanted) {
279                accumulate(&mut cot, key(input), grad)?;
280            }
281        }
282    }
283    tracers
284        .iter()
285        .map(|t| cot.remove(&key(t)).map_or_else(|| zeros_like(t), Ok))
286        .collect()
287}
288
289/// `outputs`, or the error of the first failed one: a function that returns a failed
290/// array makes the transform return its error.
291fn checked(outputs: Vec<Array>) -> Result<Vec<Array>> {
292    outputs.into_iter().map(Array::check).collect()
293}
294
295/// Reverse mode over positional arrays (the flat form of [`vjp`]): runs `f` on `primals`
296/// and returns its outputs together with the vector-Jacobian product, one gradient per
297/// primal, for one cotangent per output.
298///
299/// The results are lazy arrays. A primal that the outputs do not depend on gets a zero
300/// gradient.
301///
302/// ```
303/// use nuvec_core::{Result, array, vjp_flat};
304///
305/// # fn main() -> Result<()> {
306/// let x = array![1.0, 2.0, 3.0];
307/// let ones = x.ones_like();
308/// let (_, grads) = vjp_flat(|xs| Ok(vec![xs[0].multiply(&xs[0])]), &[x], &[ones])?;
309/// assert_eq!(grads[0].to_vec::<f32>()?, [2.0, 4.0, 6.0]);
310/// # Ok(())
311/// # }
312/// ```
313pub fn vjp_flat(
314    f: impl FnOnce(&[Array]) -> Result<Vec<Array>>,
315    primals: &[Array],
316    cotangents: &[Array],
317) -> Result<(Vec<Array>, Vec<Array>)> {
318    let tracing = Tracing::enter();
319    let tracers = primals
320        .iter()
321        .map(|p| tracing.tracer(p))
322        .collect::<Result<Vec<_>>>()?;
323    let outputs = checked(f(&tracers)?)?;
324    check_shapes("vjp", "cotangent", &outputs, cotangents)?;
325    let grads = reverse(&outputs, cotangents, &tracers)?;
326    Ok((outputs, grads))
327}
328
329/// Forward mode over positional arrays (the flat form of [`jvp`]): runs `f` on `primals`
330/// and returns its outputs together with the Jacobian-vector product, one tangent per
331/// output, for one tangent per primal.
332///
333/// ```
334/// use nuvec_core::{Array, Result, jvp_flat};
335///
336/// # fn main() -> Result<()> {
337/// let x = Array::from(0.0f32);
338/// let (_, tangents) = jvp_flat(|xs| Ok(vec![xs[0].sin()]), &[x], &[Array::from(1.0f32)])?;
339/// assert_eq!(tangents[0].item::<f32>()?, 1.0); // cos(0)
340/// # Ok(())
341/// # }
342/// ```
343pub fn jvp_flat(
344    f: impl FnOnce(&[Array]) -> Result<Vec<Array>>,
345    primals: &[Array],
346    tangents: &[Array],
347) -> Result<(Vec<Array>, Vec<Array>)> {
348    check_shapes("jvp", "tangent", primals, tangents)?;
349    let tracing = Tracing::enter();
350    let tracers = primals
351        .iter()
352        .map(|p| tracing.tracer(p))
353        .collect::<Result<Vec<_>>>()?;
354    let outputs = checked(f(&tracers)?)?;
355    let tangents = forward(&outputs, &tracers, tangents)?;
356    Ok((outputs, tangents))
357}
358
359/// Forward pass: the tangents of `outputs`, given tangents for `tracers`.
360fn forward(outputs: &[Array], tracers: &[Array], tangents: &[Array]) -> Result<Vec<Array>> {
361    let sources = tracers.iter().map(|t| Arc::as_ptr(&t.node)).collect();
362    let mut tan: NodeMap<Key, Array> = tracers
363        .iter()
364        .zip(tangents)
365        .map(|(t, v)| (key(t), v.clone()))
366        .collect();
367    for step in tape(outputs, &sources, true) {
368        let inputs: Vec<Option<Array>> = step
369            .inputs
370            .iter()
371            .map(|a| tan.get(&key(a)).cloned())
372            .collect();
373        if inputs.iter().all(Option::is_none) {
374            continue;
375        }
376        let outs = step.outputs();
377        for (o, t) in outs
378            .iter()
379            .zip(step.primitive.jvp(&step.inputs, &outs, &inputs)?)
380        {
381            tan.insert(key(o), t);
382        }
383    }
384    outputs
385        .iter()
386        .map(|o| tan.get(&key(o)).cloned().map_or_else(|| zeros_like(o), Ok))
387        .collect()
388}
389
390/// Returns a function computing `f`'s value and its gradients with respect to the
391/// positional arguments at `argnums` (the flat form of [`value_and_grad`]).
392///
393/// `f` must return a 0-D array. Only the selected arguments are traced; the others are
394/// treated as constants.
395///
396/// ```
397/// use nuvec_core::{Array, Result, array, value_and_grad_flat};
398///
399/// # fn main() -> Result<()> {
400/// // loss(w, x) = sum((w * x - 1)^2)
401/// let loss = |xs: &[Array]| {
402///     let r = xs[0].multiply(&xs[1]).subtract(1.0);
403///     Ok(r.multiply(&r).sum(..))
404/// };
405/// let (w, x) = (array![2.0], array![3.0]);
406/// let (value, grads) = value_and_grad_flat(loss, &[0])(&[w, x])?;
407/// assert_eq!(value.item::<f32>()?, 25.0);
408/// assert_eq!(grads[0].to_vec::<f32>()?, [30.0]); // 2 (w x - 1) x
409/// # Ok(())
410/// # }
411/// ```
412pub fn value_and_grad_flat<F>(
413    f: F,
414    argnums: &[usize],
415) -> impl Fn(&[Array]) -> Result<(Array, Vec<Array>)>
416where
417    F: Fn(&[Array]) -> Result<Array>,
418{
419    let argnums = argnums.to_vec();
420    move |args| {
421        if argnums.is_empty() {
422            return Err(Error::InvalidArgument(
423                "value_and_grad: no argument to differentiate".into(),
424            ));
425        }
426        let mut seen = HashSet::new();
427        for &i in &argnums {
428            if i >= args.len() || !seen.insert(i) {
429                return Err(Error::InvalidArgument(format!(
430                    "value_and_grad: invalid or repeated argument {i} for {} arguments",
431                    args.len()
432                )));
433            }
434            if !matches!(
435                args[i].dtype().category(),
436                Category::Float | Category::Complex
437            ) {
438                return Err(Error::InvalidArgument(format!(
439                    "value_and_grad: argument {i} is {}; only floating-point and complex \
440                     arrays can be differentiated",
441                    args[i].dtype()
442                )));
443            }
444        }
445        let tracing = Tracing::enter();
446        let mut inputs = args.to_vec();
447        let mut tracers = Vec::with_capacity(argnums.len());
448        for &i in &argnums {
449            inputs[i] = tracing.tracer(&args[i])?;
450            tracers.push(inputs[i].clone());
451        }
452        let value = f(&inputs)?.check()?;
453        if value.ndim() != 0 {
454            return Err(Error::InvalidArgument(format!(
455                "value_and_grad: the function must return a scalar (0-D) array, got shape {:?}",
456                value.dims()
457            )));
458        }
459        let seed = ops::filled(&[], value.dtype(), |_| Canon::Int(1))?;
460        let grads = reverse(std::slice::from_ref(&value), &[seed], &tracers)?;
461        Ok((value, grads))
462    }
463}
464
465/// Returns the gradient function of `f` with respect to the positional arguments at
466/// `argnums` (the flat form of [`grad`]).
467///
468/// ```
469/// use nuvec_core::{Array, Result, array, grad_flat};
470///
471/// # fn main() -> Result<()> {
472/// let g = grad_flat(|xs: &[Array]| Ok(xs[0].multiply(&xs[1]).sum(..)), &[1]);
473/// let grads = g(&[array![2.0], array![3.0]])?;
474/// assert_eq!(grads[0].to_vec::<f32>()?, [2.0]);
475/// # Ok(())
476/// # }
477/// ```
478pub fn grad_flat<F>(f: F, argnums: &[usize]) -> impl Fn(&[Array]) -> Result<Vec<Array>>
479where
480    F: Fn(&[Array]) -> Result<Array>,
481{
482    let value_and_grad = value_and_grad_flat(f, argnums);
483    move |args| Ok(value_and_grad(args)?.1)
484}
485
486/// A per-example placeholder for a batched `vmap` input: ops can be recorded on it, but
487/// evaluating anything that depends on it fails (as in MLX).
488fn placeholder(like: &Array, shape: &[usize], tracing: &Tracing) -> Result<Array> {
489    let failed = Array::failed(
490        Error::InvalidArgument("vmap: arrays cannot be evaluated inside vmap".into()),
491        like.dtype(),
492        &[],
493    );
494    let primitive = Primitive::Broadcast(shape.to_vec());
495    Ok(Array::record_unchecked(primitive, vec![failed], Some(tracing.id))?.swap_remove(0))
496}
497
498/// Runs `f` batched over `args` (batch axes `axes`, `None` for unbatched arguments) and
499/// returns its outputs with the batch axis first.
500pub(crate) fn batch(
501    f: &dyn Fn(&[Array]) -> Result<Vec<Array>>,
502    args: &[Array],
503    axes: &[Option<usize>],
504) -> Result<Vec<Array>> {
505    let mut size = None;
506    for (a, axis) in args.iter().zip(axes) {
507        let Some(axis) = *axis else { continue };
508        let n = *a.dims().get(axis).ok_or_else(|| {
509            Error::InvalidArgument(format!(
510                "vmap: axis {axis} out of range for shape {:?}",
511                a.dims()
512            ))
513        })?;
514        if size.is_some_and(|s| s != n) {
515            return Err(Error::InvalidArgument(format!(
516                "vmap: inconsistent batch sizes {} and {n}",
517                size.unwrap_or(0)
518            )));
519        }
520        size = Some(n);
521    }
522    let size = size
523        .ok_or_else(|| Error::InvalidArgument("vmap: at least one input must be batched".into()))?;
524    let tracing = Tracing::enter();
525    let mut batched: NodeMap<Key, (Array, usize)> = NodeMap::default();
526    let mut sources = NodeSet::default();
527    let mut inputs = Vec::with_capacity(args.len());
528    for (a, axis) in args.iter().zip(axes) {
529        inputs.push(match *axis {
530            Some(axis) => {
531                let mut shape = a.dims().to_vec();
532                shape.remove(axis);
533                let p = placeholder(a, &shape, &tracing)?;
534                sources.insert(Arc::as_ptr(&p.node));
535                batched.insert(key(&p), (a.clone(), axis));
536                p
537            }
538            None => a.clone(),
539        });
540    }
541    let outputs = checked(f(&inputs)?)?;
542    for step in tape(&outputs, &sources, false) {
543        let (arrays, axes): (Vec<Array>, Vec<Option<usize>>) = step
544            .inputs
545            .iter()
546            .map(|a| match batched.get(&key(a)) {
547                Some((b, axis)) => (b.clone(), Some(*axis)),
548                None => (a.clone(), None),
549            })
550            .unzip();
551        let (results, axis) = step.primitive.vmap(&arrays, &axes)?;
552        for (o, r) in step.outputs().iter().zip(results) {
553            batched.insert(key(o), (r, axis));
554        }
555    }
556    outputs
557        .iter()
558        .map(|o| match batched.get(&key(o)) {
559            Some((b, axis)) => crate::batching::move_axis(b, *axis, 0),
560            // Independent of every batched input: the same for each example.
561            None => {
562                let mut shape = vec![size];
563                shape.extend_from_slice(o.dims());
564                ops::broadcast_to(&ops::expand_dims(o, 0)?, &shape)
565            }
566        })
567        .collect()
568}
569
570/// `axis` (negative counts from the end) checked against `ndim` dimensions.
571fn normalize_axis(axis: isize, ndim: usize) -> Result<usize> {
572    let a = if axis < 0 { axis + ndim as isize } else { axis };
573    if (0..ndim as isize).contains(&a) {
574        Ok(a as usize)
575    } else {
576        Err(Error::InvalidArgument(format!(
577            "vmap: axis {axis} out of range for {ndim} dimensions"
578        )))
579    }
580}
581
582/// Returns `f` vectorized over a batch axis, over positional arrays (the flat form of
583/// [`vmap`]).
584///
585/// `in_axes` gives each argument's batch axis (`None` for arguments shared by every
586/// example) and `out_axes` each output's; empty slices mean axis 0 everywhere. Negative
587/// axes count from the end. Outputs that do not depend on a batched argument are broadcast
588/// along the batch axis. Arrays cannot be evaluated inside `f`.
589///
590/// ```
591/// use nuvec_core::{Result, array, vmap_flat};
592///
593/// # fn main() -> Result<()> {
594/// // A matrix-vector product per example, sharing the matrix.
595/// let mv = vmap_flat(|xs| Ok(vec![xs[0].matmul(&xs[1])]), &[None, Some(0)], &[]);
596/// let m = array![[1.0, 0.0], [0.0, 2.0]];
597/// let vs = array![[1.0, 1.0], [3.0, 4.0]]; // two vectors
598/// assert_eq!(mv(&[m, vs])?[0].to_vec::<f32>()?, [1.0, 2.0, 3.0, 8.0]);
599/// # Ok(())
600/// # }
601/// ```
602pub fn vmap_flat<F>(
603    f: F,
604    in_axes: &[Option<i32>],
605    out_axes: &[i32],
606) -> impl Fn(&[Array]) -> Result<Vec<Array>>
607where
608    F: Fn(&[Array]) -> Result<Vec<Array>>,
609{
610    let (in_axes, out_axes) = (in_axes.to_vec(), out_axes.to_vec());
611    move |args| {
612        let axes = if in_axes.is_empty() {
613            vec![Some(0); args.len()]
614        } else if in_axes.len() == args.len() {
615            in_axes
616                .iter()
617                .zip(args)
618                .map(|(axis, a)| {
619                    axis.map(|axis| normalize_axis(axis as isize, a.ndim()))
620                        .transpose()
621                })
622                .collect::<Result<Vec<_>>>()?
623        } else {
624            return Err(Error::InvalidArgument(format!(
625                "vmap: {} input axes for {} arguments",
626                in_axes.len(),
627                args.len()
628            )));
629        };
630        let outputs = batch(&f, args, &axes)?;
631        if !out_axes.is_empty() && out_axes.len() != outputs.len() {
632            return Err(Error::InvalidArgument(format!(
633                "vmap: {} output axes for {} outputs",
634                out_axes.len(),
635                outputs.len()
636            )));
637        }
638        outputs
639            .iter()
640            .enumerate()
641            .map(|(i, o)| {
642                let axis =
643                    normalize_axis(out_axes.get(i).copied().unwrap_or(0) as isize, o.ndim())?;
644                crate::batching::move_axis(o, 0, axis)
645            })
646            .collect()
647    }
648}
649
650// ── Transforms over trees ───────────────────────────────────────────────────
651
652/// A closure's return value: a tree `T`, or a `Result<T>`. So transform closures may return
653/// an array directly (`|x: &Array| x.multiply(x).sum(..)`) or a `Result` (with `?` inside).
654///
655/// The associated type makes inference unambiguous: a closure returning `Result<Array>` has
656/// `Ok = Array`, never `Ok = Result<Array>` (`Result` is not a tree).
657pub trait IntoResult {
658    /// The value.
659    type Ok;
660    /// The value, or the closure's error.
661    fn into_result(self) -> Result<Self::Ok>;
662}
663
664impl<T: Tree> IntoResult for T {
665    type Ok = T;
666    fn into_result(self) -> Result<T> {
667        Ok(self)
668    }
669}
670
671impl<T> IntoResult for Result<T> {
672    type Ok = T;
673    fn into_result(self) -> Result<T> {
674        self
675    }
676}
677
678/// What a function differentiated by [`value_and_grad`] returns: the loss alone (an
679/// [`Array`]) or the loss with auxiliary outputs (`(Array, A)` for any tree `A`, such as
680/// logits or metrics), which are returned as values, without gradients.
681pub trait GradOutput: Tree {
682    /// The 0-D loss.
683    fn loss(&self) -> &Array;
684}
685
686impl GradOutput for Array {
687    fn loss(&self) -> &Array {
688        self
689    }
690}
691
692impl<A: Tree> GradOutput for (Array, A) {
693    fn loss(&self) -> &Array {
694        &self.0
695    }
696}
697
698/// A tree's leaves and which of them are differentiated: trainable floating-point leaves.
699/// Frozen parameters, buffers and integer parameters are not (they get zero gradients); a
700/// plain `Array` leaf is always differentiated, so an integer or boolean one is an error
701/// naming it (capture such data, or make it a frozen `Param`).
702fn differentiated<T: Leaves + ?Sized>(op: &str, tree: &T) -> Result<(Vec<Array>, Vec<bool>)> {
703    struct D(Vec<Array>, Vec<bool>, Option<usize>);
704    impl Visitor for D {
705        fn leaf(&mut self, _: &str, a: &Array, kind: LeafKind, trainable: bool) {
706            let float = differentiable(a.dtype());
707            if kind == LeafKind::Array && !float && self.2.is_none() {
708                self.2 = Some(self.0.len());
709            }
710            self.1.push(trainable && float);
711            self.0.push(a.clone());
712        }
713        fn paths(&self) -> bool {
714            false
715        }
716    }
717    let mut d = D(Vec::new(), Vec::new(), None);
718    tree.visit(&mut Visit::new(&mut d));
719    if let Some(i) = d.2 {
720        // The error path: visit again for the leaf's path.
721        struct Nth(usize, Option<String>);
722        impl Visitor for Nth {
723            fn leaf(&mut self, path: &str, _: &Array, _: LeafKind, _: bool) {
724                if self.0 == 0 && self.1.is_none() {
725                    self.1 = Some(path.to_owned());
726                }
727                self.0 = self.0.wrapping_sub(1);
728            }
729        }
730        let mut nth = Nth(i, None);
731        tree.visit(&mut Visit::new(&mut nth));
732        let path = nth.1.unwrap_or_default();
733        let dtype = d.0[i].dtype();
734        return Err(Error::InvalidArgument(format!(
735            "{op}: the array {path:?} is {dtype}; only floating-point and complex arrays can be \
736             differentiated (capture it in the closure, or make it a frozen `Param`)"
737        )));
738    }
739    Ok((d.0, d.1))
740}
741
742/// `primals` with tracers for the differentiated leaves, and those tracers.
743fn traced(tracing: &Tracing, leaves: &[Array], mask: &[bool]) -> Result<(Vec<Array>, Vec<Array>)> {
744    let mut inputs = Vec::with_capacity(leaves.len());
745    let mut tracers = Vec::new();
746    for (a, &d) in leaves.iter().zip(mask) {
747        if d {
748            let t = tracing.tracer(a)?;
749            tracers.push(t.clone());
750            inputs.push(t);
751        } else {
752            inputs.push(a.clone());
753        }
754    }
755    Ok((inputs, tracers))
756}
757
758/// Gradients for every leaf: `grads` for the differentiated ones (in order), stride-0
759/// zeros (one element, broadcast) for the others.
760fn assemble(leaves: &[Array], mask: &[bool], grads: Vec<Array>) -> Vec<Array> {
761    let mut grads = grads.into_iter();
762    leaves
763        .iter()
764        .zip(mask)
765        .map(|(a, &d)| match d {
766            true => grads.next().expect("one gradient per differentiated leaf"),
767            false => broadcast_zero(a),
768        })
769        .collect()
770}
771
772/// Checks that `given` (cotangents or tangents) has `like`'s paths, shapes and dtypes.
773fn check_tree<A: Tree, B: Tree>(op: &str, what: &str, like: &A, given: &B) -> Result<()> {
774    let (lp, gp) = (like.flatten(), given.flatten());
775    if lp.len() != gp.len() {
776        return Err(Error::InvalidArgument(format!(
777            "{op}: {} {what} leaves for {} leaves",
778            gp.len(),
779            lp.len()
780        )));
781    }
782    for ((p, a), (q, g)) in lp.iter().zip(&gp) {
783        if p != q {
784            return Err(Error::InvalidArgument(format!(
785                "{op}: the {what} leaf {q:?} is not at the path {p:?}"
786            )));
787        }
788        if a.shape() != g.shape() || a.dtype() != g.dtype() {
789            return Err(Error::InvalidArgument(format!(
790                "{op}: the {what} at {p:?} is {} {:?}, expected {} {:?}",
791                g.dtype(),
792                g.shape(),
793                a.dtype(),
794                a.shape()
795            )));
796        }
797    }
798    Ok(())
799}
800
801/// The value of `f` at `params` and its gradient with respect to the trainable leaves.
802fn value_and_grad_tree<P: Tree, O: GradOutput>(
803    f: impl FnOnce(&P) -> Result<O>,
804    params: &P,
805) -> Result<(O, P)> {
806    let (leaves, mask) = differentiated("value_and_grad", params)?;
807    let tracing = Tracing::enter();
808    let (inputs, tracers) = traced(&tracing, &leaves, &mask)?;
809    let out = f(&params.unflatten(inputs)?)?;
810    let loss = out.loss().clone().check()?;
811    if loss.ndim() != 0 {
812        return Err(Error::InvalidArgument(format!(
813            "value_and_grad: the function must return a scalar (0-D) loss, got shape {:?}",
814            loss.dims()
815        )));
816    }
817    let seed = ops::filled(&[], loss.dtype(), |_| Canon::Int(1))?;
818    let grads = reverse(std::slice::from_ref(&loss), &[seed], &tracers)?;
819    let grads = params.unflatten(assemble(&leaves, &mask, grads))?;
820    Ok((out, grads))
821}
822
823/// Returns a function computing `f`'s value and its gradient with respect to the trainable
824/// leaves of its argument, as a tree of the argument's type (`grads.layers[0].weight`).
825///
826/// `f` takes the differentiated tree (a tuple, a struct, a model) and returns the 0-D loss,
827/// or `(loss, aux)` to also return auxiliary outputs (the return type decides); it may
828/// return the value directly or a `Result`. Constants, such as a batch, are captured by
829/// the closure. Write the closure's parameter type: `|p: &(Array, Array)|`, `|m: &Mlp|`
830/// (Rust cannot infer it through the generic transform: E0282 otherwise).
831///
832/// Frozen parameters, buffers and integer parameters get zero gradients that allocate no
833/// full-size memory (stride-0 broadcasts of one element); a plain integer or boolean
834/// `Array` leaf is an error (capture such data in the closure instead).
835///
836/// ```
837/// use nuvec_core::{Array, Result, array, value_and_grad};
838///
839/// # fn main() -> Result<()> {
840/// let x = array![1.0, 2.0];
841/// // loss(w, b) = sum((w x + b)^2), with `x` captured.
842/// let loss = |(w, b): &(Array, Array)| {
843///     let r = w.multiply(&x).add(b);
844///     r.multiply(&r).sum(..)
845/// };
846/// let (value, (gw, gb)) = value_and_grad(loss)(&(array![1.0, 1.0], array![0.0, 0.0]))?;
847/// assert_eq!(value.item::<f32>()?, 5.0);
848/// assert_eq!(gw.to_vec::<f32>()?, [2.0, 8.0]); // 2 (w x + b) x
849/// assert_eq!(gb.to_vec::<f32>()?, [2.0, 4.0]);
850/// # Ok(())
851/// # }
852/// ```
853pub fn value_and_grad<P, F, R>(f: F) -> impl Fn(&P) -> Result<(R::Ok, P)>
854where
855    P: Tree,
856    F: Fn(&P) -> R,
857    R: IntoResult,
858    R::Ok: GradOutput,
859{
860    move |params| value_and_grad_tree(|p: &P| f(p).into_result(), params)
861}
862
863/// Returns the gradient function of `f` (a scalar function of a tree): the gradient with
864/// respect to the trainable leaves, as a tree of the argument's type (see
865/// [`value_and_grad`]).
866///
867/// Gradient functions can be differentiated again:
868///
869/// ```
870/// use nuvec_core::{Array, Result, grad};
871///
872/// # fn main() -> Result<()> {
873/// let d2 = grad(grad(|x: &Array| x.sin()));
874/// let x = Array::from(1.0f32);
875/// assert!((d2(&x)?.item::<f32>()? + 1f32.sin()).abs() < 1e-6); // −sin(x)
876/// # Ok(())
877/// # }
878/// ```
879pub fn grad<P, F, R>(f: F) -> impl Fn(&P) -> Result<P>
880where
881    P: Tree,
882    F: Fn(&P) -> R,
883    R: IntoResult<Ok = Array>,
884{
885    move |params| Ok(value_and_grad_tree(|p: &P| f(p).into_result(), params)?.1)
886}
887
888/// Reverse mode over trees: `f`'s output at `primals` and the vector-Jacobian product for
889/// `cotangents` (a tree like the output: same paths, shapes and dtypes, else an error
890/// naming the path), as a tree like `primals` (zeros for leaves that are not
891/// differentiated).
892///
893/// ```
894/// use nuvec_core::{Array, Result, array, vjp};
895///
896/// # fn main() -> Result<()> {
897/// let x = array![1.0, 2.0, 3.0];
898/// let (_, g) = vjp(|x: &Array| x.multiply(x), &x, &x.ones_like())?;
899/// assert_eq!(g.to_vec::<f32>()?, [2.0, 4.0, 6.0]);
900/// # Ok(())
901/// # }
902/// ```
903pub fn vjp<P, O, F, R>(f: F, primals: &P, cotangents: &O) -> Result<(O, P)>
904where
905    P: Tree,
906    O: Tree,
907    F: FnOnce(&P) -> R,
908    R: IntoResult<Ok = O>,
909{
910    let (leaves, mask) = differentiated("vjp", primals)?;
911    let tracing = Tracing::enter();
912    let (inputs, tracers) = traced(&tracing, &leaves, &mask)?;
913    let out = f(&primals.unflatten(inputs)?).into_result()?;
914    let outputs = checked(out.leaves())?;
915    check_tree("vjp", "cotangent", &out, cotangents)?;
916    let grads = reverse(&outputs, &cotangents.leaves(), &tracers)?;
917    let grads = primals.unflatten(assemble(&leaves, &mask, grads))?;
918    Ok((out.unflatten(outputs)?, grads))
919}
920
921/// Forward mode over trees: `f`'s output at `primals` and the Jacobian-vector product for
922/// `tangents` (a tree like `primals`; tangents of leaves that are not differentiated are
923/// ignored), as a tree like the output.
924///
925/// ```
926/// use nuvec_core::{Array, Result, jvp};
927///
928/// # fn main() -> Result<()> {
929/// let (y, t) = jvp(|x: &Array| x.sin(), &Array::from(0.0f32), &Array::from(1.0f32))?;
930/// assert_eq!((y.item::<f32>()?, t.item::<f32>()?), (0.0, 1.0)); // cos(0)
931/// # Ok(())
932/// # }
933/// ```
934pub fn jvp<P, O, F, R>(f: F, primals: &P, tangents: &P) -> Result<(O, O)>
935where
936    P: Tree,
937    O: Tree,
938    F: FnOnce(&P) -> R,
939    R: IntoResult<Ok = O>,
940{
941    check_tree("jvp", "tangent", primals, tangents)?;
942    let (leaves, mask) = differentiated("jvp", primals)?;
943    let tangents: Vec<Array> = tangents
944        .leaves()
945        .into_iter()
946        .zip(&mask)
947        .filter_map(|(t, &d)| d.then_some(t))
948        .collect();
949    let tracing = Tracing::enter();
950    let (inputs, tracers) = traced(&tracing, &leaves, &mask)?;
951    let out = f(&primals.unflatten(inputs)?).into_result()?;
952    let outputs = checked(out.leaves())?;
953    let out_tangents = forward(&outputs, &tracers, &tangents)?;
954    Ok((out.unflatten(outputs)?, out.unflatten(out_tangents)?))
955}
956
957/// One batch axis per top-level argument of a [`Vmap`] (`None`: not batched): an `i32` or
958/// `Option<i32>` (one axis for every argument) or a tuple of them (one per argument).
959pub trait InAxes {
960    /// The axes, one per top-level argument (one entry: every argument).
961    fn axes(self) -> Vec<Option<i32>>;
962}
963
964/// One entry of an [`InAxes`] tuple: an `i32` or `Option<i32>`.
965pub trait InAxis {
966    /// The axis.
967    fn axis(self) -> Option<i32>;
968}
969
970impl InAxis for i32 {
971    fn axis(self) -> Option<i32> {
972        Some(self)
973    }
974}
975
976impl InAxis for Option<i32> {
977    fn axis(self) -> Option<i32> {
978        self
979    }
980}
981
982impl InAxes for i32 {
983    fn axes(self) -> Vec<Option<i32>> {
984        vec![Some(self)]
985    }
986}
987
988impl InAxes for Option<i32> {
989    fn axes(self) -> Vec<Option<i32>> {
990        vec![self]
991    }
992}
993
994impl InAxes for Vec<Option<i32>> {
995    fn axes(self) -> Vec<Option<i32>> {
996        self
997    }
998}
999
1000/// One output axis per top-level output of a [`Vmap`]: an `i32` (every output) or a tuple
1001/// of them.
1002pub trait OutAxes {
1003    /// The axes, one per top-level output (one entry: every output).
1004    fn axes(self) -> Vec<i32>;
1005}
1006
1007impl OutAxes for i32 {
1008    fn axes(self) -> Vec<i32> {
1009        vec![self]
1010    }
1011}
1012
1013impl OutAxes for Vec<i32> {
1014    fn axes(self) -> Vec<i32> {
1015        self
1016    }
1017}
1018
1019macro_rules! axes_tuples {
1020    ($(($($t:ident $i:tt),+)),*) => {$(
1021        impl<$($t: InAxis),+> InAxes for ($($t,)+) {
1022            fn axes(self) -> Vec<Option<i32>> {
1023                vec![$(self.$i.axis()),+]
1024            }
1025        }
1026        impl OutAxes for ($(axes_tuples!(@i32 $t),)+) {
1027            fn axes(self) -> Vec<i32> {
1028                vec![$(self.$i),+]
1029            }
1030        }
1031    )*};
1032    (@i32 $t:ident) => { i32 };
1033}
1034
1035axes_tuples!(
1036    (A 0, B 1),
1037    (A 0, B 1, C 2),
1038    (A 0, B 1, C 2, D 3),
1039    (A 0, B 1, C 2, D 3, E 4),
1040    (A 0, B 1, C 2, D 3, E 4, F 5),
1041    (A 0, B 1, C 2, D 3, E 4, F 5, G 6),
1042    (A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7)
1043);
1044
1045/// The top-level argument of each leaf: the index of its path's first segment among the
1046/// distinct first segments, in order; and how many there are.
1047fn top_level(paths: &[String]) -> (Vec<usize>, usize) {
1048    let mut firsts: Vec<&str> = Vec::new();
1049    let groups = paths
1050        .iter()
1051        .map(|p| {
1052            let first = p.split('.').next().unwrap_or("");
1053            match firsts.iter().position(|f| *f == first) {
1054                Some(i) => i,
1055                None => {
1056                    firsts.push(first);
1057                    firsts.len() - 1
1058                }
1059            }
1060        })
1061        .collect();
1062    (groups, firsts.len())
1063}
1064
1065/// A function vectorized over batch axes ([`vmap`]): configure it with
1066/// [`Vmap::in_axes`]/[`Vmap::out_axes`], then call it with [`Vmap::call`] or turn it into a
1067/// closure with [`Vmap::into_fn`] (to compose with `compile` and the other transforms).
1068pub struct Vmap<I, O, F> {
1069    f: F,
1070    in_axes: Vec<Option<i32>>,
1071    out_axes: Vec<i32>,
1072    types: PhantomData<fn(&I) -> O>,
1073}
1074
1075/// Returns `f` vectorized over a batch axis of its inputs (axis 0 of every top-level
1076/// argument and output by default; see [`Vmap::in_axes`] and [`Vmap::out_axes`]).
1077///
1078/// `f` takes a tree (an array, a tuple of arguments, a struct) and returns one. Outputs
1079/// that do not depend on a batched input are broadcast along the batch axis. Arrays cannot
1080/// be evaluated inside `f`.
1081///
1082/// ```
1083/// use nuvec_core::{Array, Result, array, vmap};
1084///
1085/// # fn main() -> Result<()> {
1086/// // A matrix-vector product per example, sharing the matrix.
1087/// let mv = vmap(|(m, v): &(Array, Array)| m.matmul(v)).in_axes((None, 0));
1088/// let m = array![[1.0, 0.0], [0.0, 2.0]];
1089/// let vs = array![[1.0, 1.0], [3.0, 4.0]]; // two vectors
1090/// assert_eq!(mv.call(&(m, vs))?.to_vec::<f32>()?, [1.0, 2.0, 3.0, 8.0]);
1091/// # Ok(())
1092/// # }
1093/// ```
1094pub fn vmap<I, O, F, R>(f: F) -> Vmap<I, O, F>
1095where
1096    I: Tree,
1097    O: Tree,
1098    F: Fn(&I) -> R,
1099    R: IntoResult<Ok = O>,
1100{
1101    Vmap {
1102        f,
1103        in_axes: vec![Some(0)],
1104        out_axes: vec![0],
1105        types: PhantomData,
1106    }
1107}
1108
1109impl<I: Tree, O: Tree, F> Vmap<I, O, F> {
1110    /// The batch axis of each top-level argument (`None`: shared by every example), or one
1111    /// axis for all: `in_axes((None, 0))`, `in_axes(1)`. Negative axes count from the end.
1112    pub fn in_axes(mut self, axes: impl InAxes) -> Self {
1113        self.in_axes = axes.axes();
1114        self
1115    }
1116
1117    /// The axis where each top-level output gets the batch dimension, or one for all.
1118    pub fn out_axes(mut self, axes: impl OutAxes) -> Self {
1119        self.out_axes = axes.axes();
1120        self
1121    }
1122
1123    /// `f` batched over `inputs`.
1124    pub fn call<R>(&self, inputs: &I) -> Result<O>
1125    where
1126        F: Fn(&I) -> R,
1127        R: IntoResult<Ok = O>,
1128    {
1129        let named = inputs.flatten();
1130        let paths: Vec<String> = named.iter().map(|(p, _)| p.clone()).collect();
1131        let args: Vec<Array> = named.into_iter().map(|(_, a)| a).collect();
1132        let (groups, n) = top_level(&paths);
1133        let per_arg = |k: usize| match self.in_axes.as_slice() {
1134            [one] => Ok(*one),
1135            axes if axes.len() == n => Ok(axes[groups[k]]),
1136            axes => Err(Error::InvalidArgument(format!(
1137                "vmap: {} input axes for {n} top-level arguments",
1138                axes.len()
1139            ))),
1140        };
1141        let axes = args
1142            .iter()
1143            .enumerate()
1144            .map(|(k, a)| {
1145                per_arg(k)?
1146                    .map(|axis| normalize_axis(axis as isize, a.ndim()))
1147                    .transpose()
1148                    .map_err(|e| match e {
1149                        Error::InvalidArgument(m) => {
1150                            Error::InvalidArgument(format!("{m} (argument {:?})", paths[k]))
1151                        }
1152                        e => e,
1153                    })
1154            })
1155            .collect::<Result<Vec<_>>>()?;
1156        let template: RefCell<Option<O>> = RefCell::new(None);
1157        let body = |xs: &[Array]| -> Result<Vec<Array>> {
1158            let out = (self.f)(&inputs.unflatten(xs.iter().cloned())?).into_result()?;
1159            let leaves = out.leaves();
1160            *template.borrow_mut() = Some(out);
1161            Ok(leaves)
1162        };
1163        let outputs = batch(&body, &args, &axes)?;
1164        let template = template
1165            .into_inner()
1166            .ok_or_else(|| Error::InvalidArgument("vmap: the function did not run".into()))?;
1167        let out_paths = template.paths();
1168        let (out_groups, m) = top_level(&out_paths);
1169        let moved = outputs
1170            .iter()
1171            .enumerate()
1172            .map(|(k, o)| {
1173                let axis = match self.out_axes.as_slice() {
1174                    [one] => *one,
1175                    axes if axes.len() == m => axes[out_groups[k]],
1176                    axes => {
1177                        return Err(Error::InvalidArgument(format!(
1178                            "vmap: {} output axes for {m} top-level outputs",
1179                            axes.len()
1180                        )));
1181                    }
1182                };
1183                let axis = normalize_axis(axis as isize, o.ndim())?;
1184                crate::batching::move_axis(o, 0, axis)
1185            })
1186            .collect::<Result<Vec<_>>>()?;
1187        template.unflatten(moved)
1188    }
1189
1190    /// The configured batched function as a closure, which composes with `compile`, `grad`
1191    /// and the other transforms.
1192    pub fn into_fn<R>(self) -> impl Fn(&I) -> Result<O>
1193    where
1194        F: Fn(&I) -> R,
1195        R: IntoResult<Ok = O>,
1196    {
1197        move |inputs| self.call(inputs)
1198    }
1199}