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        // A rule's results checked (custom rules are user code): one per output, each
553        // with the batch axis.
554        let outs = step.outputs();
555        if results.len() != outs.len() || results.iter().any(|r| axis >= r.ndim()) {
556            return Err(Error::InvalidArgument(format!(
557                "vmap: the batching rule of {} returned {} outputs with batch axis {axis} \
558                 ({} expected, each with that axis)",
559                step.primitive.name(),
560                results.len(),
561                outs.len()
562            )));
563        }
564        for (o, r) in outs.iter().zip(results) {
565            batched.insert(key(o), (r, axis));
566        }
567    }
568    outputs
569        .iter()
570        .map(|o| match batched.get(&key(o)) {
571            Some((b, axis)) => crate::batching::move_axis(b, *axis, 0),
572            // Independent of every batched input: the same for each example.
573            None => {
574                let mut shape = vec![size];
575                shape.extend_from_slice(o.dims());
576                ops::broadcast_to(&ops::expand_dims(o, 0)?, &shape)
577            }
578        })
579        .collect()
580}
581
582/// `axis` (negative counts from the end) checked against `ndim` dimensions.
583fn normalize_axis(axis: isize, ndim: usize) -> Result<usize> {
584    let a = if axis < 0 { axis + ndim as isize } else { axis };
585    if (0..ndim as isize).contains(&a) {
586        Ok(a as usize)
587    } else {
588        Err(Error::InvalidArgument(format!(
589            "vmap: axis {axis} out of range for {ndim} dimensions"
590        )))
591    }
592}
593
594/// Returns `f` vectorized over a batch axis, over positional arrays (the flat form of
595/// [`vmap`]).
596///
597/// `in_axes` gives each argument's batch axis (`None` for arguments shared by every
598/// example) and `out_axes` each output's; empty slices mean axis 0 everywhere. Negative
599/// axes count from the end. Outputs that do not depend on a batched argument are broadcast
600/// along the batch axis. Arrays cannot be evaluated inside `f`.
601///
602/// ```
603/// use nuvec_core::{Result, array, vmap_flat};
604///
605/// # fn main() -> Result<()> {
606/// // A matrix-vector product per example, sharing the matrix.
607/// let mv = vmap_flat(|xs| Ok(vec![xs[0].matmul(&xs[1])]), &[None, Some(0)], &[]);
608/// let m = array![[1.0, 0.0], [0.0, 2.0]];
609/// let vs = array![[1.0, 1.0], [3.0, 4.0]]; // two vectors
610/// assert_eq!(mv(&[m, vs])?[0].to_vec::<f32>()?, [1.0, 2.0, 3.0, 8.0]);
611/// # Ok(())
612/// # }
613/// ```
614pub fn vmap_flat<F>(
615    f: F,
616    in_axes: &[Option<i32>],
617    out_axes: &[i32],
618) -> impl Fn(&[Array]) -> Result<Vec<Array>>
619where
620    F: Fn(&[Array]) -> Result<Vec<Array>>,
621{
622    let (in_axes, out_axes) = (in_axes.to_vec(), out_axes.to_vec());
623    move |args| {
624        let axes = if in_axes.is_empty() {
625            vec![Some(0); args.len()]
626        } else if in_axes.len() == args.len() {
627            in_axes
628                .iter()
629                .zip(args)
630                .map(|(axis, a)| {
631                    axis.map(|axis| normalize_axis(axis as isize, a.ndim()))
632                        .transpose()
633                })
634                .collect::<Result<Vec<_>>>()?
635        } else {
636            return Err(Error::InvalidArgument(format!(
637                "vmap: {} input axes for {} arguments",
638                in_axes.len(),
639                args.len()
640            )));
641        };
642        let outputs = batch(&f, args, &axes)?;
643        if !out_axes.is_empty() && out_axes.len() != outputs.len() {
644            return Err(Error::InvalidArgument(format!(
645                "vmap: {} output axes for {} outputs",
646                out_axes.len(),
647                outputs.len()
648            )));
649        }
650        outputs
651            .iter()
652            .enumerate()
653            .map(|(i, o)| {
654                let axis =
655                    normalize_axis(out_axes.get(i).copied().unwrap_or(0) as isize, o.ndim())?;
656                crate::batching::move_axis(o, 0, axis)
657            })
658            .collect()
659    }
660}
661
662// ── Transforms over trees ───────────────────────────────────────────────────
663
664/// A closure's return value: a tree `T`, or a `Result<T>`. So transform closures may return
665/// an array directly (`|x: &Array| x.multiply(x).sum(..)`) or a `Result` (with `?` inside).
666///
667/// The associated type makes inference unambiguous: a closure returning `Result<Array>` has
668/// `Ok = Array`, never `Ok = Result<Array>` (`Result` is not a tree).
669pub trait IntoResult {
670    /// The value.
671    type Ok;
672    /// The value, or the closure's error.
673    fn into_result(self) -> Result<Self::Ok>;
674}
675
676impl<T: Tree> IntoResult for T {
677    type Ok = T;
678    fn into_result(self) -> Result<T> {
679        Ok(self)
680    }
681}
682
683impl<T> IntoResult for Result<T> {
684    type Ok = T;
685    fn into_result(self) -> Result<T> {
686        self
687    }
688}
689
690/// What a function differentiated by [`value_and_grad`] returns: the loss alone (an
691/// [`Array`]) or the loss with auxiliary outputs (`(Array, A)` for any tree `A`, such as
692/// logits or metrics), which are returned as values, without gradients.
693pub trait GradOutput: Tree {
694    /// The 0-D loss.
695    fn loss(&self) -> &Array;
696}
697
698impl GradOutput for Array {
699    fn loss(&self) -> &Array {
700        self
701    }
702}
703
704impl<A: Tree> GradOutput for (Array, A) {
705    fn loss(&self) -> &Array {
706        &self.0
707    }
708}
709
710/// A tree's leaves and which of them are differentiated: trainable floating-point leaves.
711/// Frozen parameters, buffers and integer parameters are not (they get zero gradients); a
712/// plain `Array` leaf is always differentiated, so an integer or boolean one is an error
713/// naming it (capture such data, or make it a frozen `Param`).
714fn differentiated<T: Leaves + ?Sized>(op: &str, tree: &T) -> Result<(Vec<Array>, Vec<bool>)> {
715    struct D(Vec<Array>, Vec<bool>, Option<usize>);
716    impl Visitor for D {
717        fn leaf(&mut self, _: &str, a: &Array, kind: LeafKind, trainable: bool) {
718            let float = differentiable(a.dtype());
719            if kind == LeafKind::Array && !float && self.2.is_none() {
720                self.2 = Some(self.0.len());
721            }
722            self.1.push(trainable && float);
723            self.0.push(a.clone());
724        }
725        fn paths(&self) -> bool {
726            false
727        }
728    }
729    let mut d = D(Vec::new(), Vec::new(), None);
730    tree.visit(&mut Visit::new(&mut d));
731    if let Some(i) = d.2 {
732        // The error path: visit again for the leaf's path.
733        struct Nth(usize, Option<String>);
734        impl Visitor for Nth {
735            fn leaf(&mut self, path: &str, _: &Array, _: LeafKind, _: bool) {
736                if self.0 == 0 && self.1.is_none() {
737                    self.1 = Some(path.to_owned());
738                }
739                self.0 = self.0.wrapping_sub(1);
740            }
741        }
742        let mut nth = Nth(i, None);
743        tree.visit(&mut Visit::new(&mut nth));
744        let path = nth.1.unwrap_or_default();
745        let dtype = d.0[i].dtype();
746        return Err(Error::InvalidArgument(format!(
747            "{op}: the array {path:?} is {dtype}; only floating-point and complex arrays can be \
748             differentiated (capture it in the closure, or make it a frozen `Param`)"
749        )));
750    }
751    Ok((d.0, d.1))
752}
753
754/// `primals` with tracers for the differentiated leaves, and those tracers.
755fn traced(tracing: &Tracing, leaves: &[Array], mask: &[bool]) -> Result<(Vec<Array>, Vec<Array>)> {
756    let mut inputs = Vec::with_capacity(leaves.len());
757    let mut tracers = Vec::new();
758    for (a, &d) in leaves.iter().zip(mask) {
759        if d {
760            let t = tracing.tracer(a)?;
761            tracers.push(t.clone());
762            inputs.push(t);
763        } else {
764            inputs.push(a.clone());
765        }
766    }
767    Ok((inputs, tracers))
768}
769
770/// Gradients for every leaf: `grads` for the differentiated ones (in order), stride-0
771/// zeros (one element, broadcast) for the others.
772fn assemble(leaves: &[Array], mask: &[bool], grads: Vec<Array>) -> Vec<Array> {
773    let mut grads = grads.into_iter();
774    leaves
775        .iter()
776        .zip(mask)
777        .map(|(a, &d)| match d {
778            true => grads.next().expect("one gradient per differentiated leaf"),
779            false => broadcast_zero(a),
780        })
781        .collect()
782}
783
784/// Checks that `given` (cotangents or tangents) has `like`'s paths, shapes and dtypes.
785fn check_tree<A: Tree, B: Tree>(op: &str, what: &str, like: &A, given: &B) -> Result<()> {
786    let (lp, gp) = (like.flatten(), given.flatten());
787    if lp.len() != gp.len() {
788        return Err(Error::InvalidArgument(format!(
789            "{op}: {} {what} leaves for {} leaves",
790            gp.len(),
791            lp.len()
792        )));
793    }
794    for ((p, a), (q, g)) in lp.iter().zip(&gp) {
795        if p != q {
796            return Err(Error::InvalidArgument(format!(
797                "{op}: the {what} leaf {q:?} is not at the path {p:?}"
798            )));
799        }
800        if a.shape() != g.shape() || a.dtype() != g.dtype() {
801            return Err(Error::InvalidArgument(format!(
802                "{op}: the {what} at {p:?} is {} {:?}, expected {} {:?}",
803                g.dtype(),
804                g.shape(),
805                a.dtype(),
806                a.shape()
807            )));
808        }
809    }
810    Ok(())
811}
812
813/// The value of `f` at `params` and its gradient with respect to the trainable leaves.
814fn value_and_grad_tree<P: Tree, O: GradOutput>(
815    f: impl FnOnce(&P) -> Result<O>,
816    params: &P,
817) -> Result<(O, P)> {
818    let (leaves, mask) = differentiated("value_and_grad", params)?;
819    let tracing = Tracing::enter();
820    let (inputs, tracers) = traced(&tracing, &leaves, &mask)?;
821    let out = f(&params.unflatten(inputs)?)?;
822    let loss = out.loss().clone().check()?;
823    if loss.ndim() != 0 {
824        return Err(Error::InvalidArgument(format!(
825            "value_and_grad: the function must return a scalar (0-D) loss, got shape {:?}",
826            loss.dims()
827        )));
828    }
829    let seed = ops::filled(&[], loss.dtype(), |_| Canon::Int(1))?;
830    let grads = reverse(std::slice::from_ref(&loss), &[seed], &tracers)?;
831    let grads = params.unflatten(assemble(&leaves, &mask, grads))?;
832    Ok((out, grads))
833}
834
835/// Returns a function computing `f`'s value and its gradient with respect to the trainable
836/// leaves of its argument, as a tree of the argument's type (`grads.layers[0].weight`).
837///
838/// `f` takes the differentiated tree (a tuple, a struct, a model) and returns the 0-D loss,
839/// or `(loss, aux)` to also return auxiliary outputs (the return type decides); it may
840/// return the value directly or a `Result`. Constants, such as a batch, are captured by
841/// the closure. Write the closure's parameter type: `|p: &(Array, Array)|`, `|m: &Mlp|`
842/// (Rust cannot infer it through the generic transform: E0282 otherwise).
843///
844/// Frozen parameters, buffers and integer parameters get zero gradients that allocate no
845/// full-size memory (stride-0 broadcasts of one element); a plain integer or boolean
846/// `Array` leaf is an error (capture such data in the closure instead).
847///
848/// ```
849/// use nuvec_core::{Array, Result, array, value_and_grad};
850///
851/// # fn main() -> Result<()> {
852/// let x = array![1.0, 2.0];
853/// // loss(w, b) = sum((w x + b)^2), with `x` captured.
854/// let loss = |(w, b): &(Array, Array)| {
855///     let r = w.multiply(&x).add(b);
856///     r.multiply(&r).sum(..)
857/// };
858/// let (value, (gw, gb)) = value_and_grad(loss)(&(array![1.0, 1.0], array![0.0, 0.0]))?;
859/// assert_eq!(value.item::<f32>()?, 5.0);
860/// assert_eq!(gw.to_vec::<f32>()?, [2.0, 8.0]); // 2 (w x + b) x
861/// assert_eq!(gb.to_vec::<f32>()?, [2.0, 4.0]);
862/// # Ok(())
863/// # }
864/// ```
865pub fn value_and_grad<P, F, R>(f: F) -> impl Fn(&P) -> Result<(R::Ok, P)>
866where
867    P: Tree,
868    F: Fn(&P) -> R,
869    R: IntoResult,
870    R::Ok: GradOutput,
871{
872    move |params| value_and_grad_tree(|p: &P| f(p).into_result(), params)
873}
874
875/// Returns the gradient function of `f` (a scalar function of a tree): the gradient with
876/// respect to the trainable leaves, as a tree of the argument's type (see
877/// [`value_and_grad`]).
878///
879/// Gradient functions can be differentiated again:
880///
881/// ```
882/// use nuvec_core::{Array, Result, grad};
883///
884/// # fn main() -> Result<()> {
885/// let d2 = grad(grad(|x: &Array| x.sin()));
886/// let x = Array::from(1.0f32);
887/// assert!((d2(&x)?.item::<f32>()? + 1f32.sin()).abs() < 1e-6); // −sin(x)
888/// # Ok(())
889/// # }
890/// ```
891pub fn grad<P, F, R>(f: F) -> impl Fn(&P) -> Result<P>
892where
893    P: Tree,
894    F: Fn(&P) -> R,
895    R: IntoResult<Ok = Array>,
896{
897    move |params| Ok(value_and_grad_tree(|p: &P| f(p).into_result(), params)?.1)
898}
899
900/// Reverse mode over trees: `f`'s output at `primals` and the vector-Jacobian product for
901/// `cotangents` (a tree like the output: same paths, shapes and dtypes, else an error
902/// naming the path), as a tree like `primals` (zeros for leaves that are not
903/// differentiated).
904///
905/// ```
906/// use nuvec_core::{Array, Result, array, vjp};
907///
908/// # fn main() -> Result<()> {
909/// let x = array![1.0, 2.0, 3.0];
910/// let (_, g) = vjp(|x: &Array| x.multiply(x), &x, &x.ones_like())?;
911/// assert_eq!(g.to_vec::<f32>()?, [2.0, 4.0, 6.0]);
912/// # Ok(())
913/// # }
914/// ```
915pub fn vjp<P, O, F, R>(f: F, primals: &P, cotangents: &O) -> Result<(O, P)>
916where
917    P: Tree,
918    O: Tree,
919    F: FnOnce(&P) -> R,
920    R: IntoResult<Ok = O>,
921{
922    let (leaves, mask) = differentiated("vjp", primals)?;
923    let tracing = Tracing::enter();
924    let (inputs, tracers) = traced(&tracing, &leaves, &mask)?;
925    let out = f(&primals.unflatten(inputs)?).into_result()?;
926    let outputs = checked(out.leaves())?;
927    check_tree("vjp", "cotangent", &out, cotangents)?;
928    let grads = reverse(&outputs, &cotangents.leaves(), &tracers)?;
929    let grads = primals.unflatten(assemble(&leaves, &mask, grads))?;
930    Ok((out.unflatten(outputs)?, grads))
931}
932
933/// Forward mode over trees: `f`'s output at `primals` and the Jacobian-vector product for
934/// `tangents` (a tree like `primals`; tangents of leaves that are not differentiated are
935/// ignored), as a tree like the output.
936///
937/// ```
938/// use nuvec_core::{Array, Result, jvp};
939///
940/// # fn main() -> Result<()> {
941/// let (y, t) = jvp(|x: &Array| x.sin(), &Array::from(0.0f32), &Array::from(1.0f32))?;
942/// assert_eq!((y.item::<f32>()?, t.item::<f32>()?), (0.0, 1.0)); // cos(0)
943/// # Ok(())
944/// # }
945/// ```
946pub fn jvp<P, O, F, R>(f: F, primals: &P, tangents: &P) -> Result<(O, O)>
947where
948    P: Tree,
949    O: Tree,
950    F: FnOnce(&P) -> R,
951    R: IntoResult<Ok = O>,
952{
953    check_tree("jvp", "tangent", primals, tangents)?;
954    let (leaves, mask) = differentiated("jvp", primals)?;
955    let tangents: Vec<Array> = tangents
956        .leaves()
957        .into_iter()
958        .zip(&mask)
959        .filter_map(|(t, &d)| d.then_some(t))
960        .collect();
961    let tracing = Tracing::enter();
962    let (inputs, tracers) = traced(&tracing, &leaves, &mask)?;
963    let out = f(&primals.unflatten(inputs)?).into_result()?;
964    let outputs = checked(out.leaves())?;
965    let out_tangents = forward(&outputs, &tracers, &tangents)?;
966    Ok((out.unflatten(outputs)?, out.unflatten(out_tangents)?))
967}
968
969/// One batch axis per top-level argument of a [`Vmap`] (`None`: not batched): an `i32` or
970/// `Option<i32>` (one axis for every argument) or a tuple of them (one per argument).
971pub trait InAxes {
972    /// The axes, one per top-level argument (one entry: every argument).
973    fn axes(self) -> Vec<Option<i32>>;
974}
975
976/// One entry of an [`InAxes`] tuple: an `i32` or `Option<i32>`.
977pub trait InAxis {
978    /// The axis.
979    fn axis(self) -> Option<i32>;
980}
981
982impl InAxis for i32 {
983    fn axis(self) -> Option<i32> {
984        Some(self)
985    }
986}
987
988impl InAxis for Option<i32> {
989    fn axis(self) -> Option<i32> {
990        self
991    }
992}
993
994impl InAxes for i32 {
995    fn axes(self) -> Vec<Option<i32>> {
996        vec![Some(self)]
997    }
998}
999
1000impl InAxes for Option<i32> {
1001    fn axes(self) -> Vec<Option<i32>> {
1002        vec![self]
1003    }
1004}
1005
1006impl InAxes for Vec<Option<i32>> {
1007    fn axes(self) -> Vec<Option<i32>> {
1008        self
1009    }
1010}
1011
1012/// One output axis per top-level output of a [`Vmap`]: an `i32` (every output) or a tuple
1013/// of them.
1014pub trait OutAxes {
1015    /// The axes, one per top-level output (one entry: every output).
1016    fn axes(self) -> Vec<i32>;
1017}
1018
1019impl OutAxes for i32 {
1020    fn axes(self) -> Vec<i32> {
1021        vec![self]
1022    }
1023}
1024
1025impl OutAxes for Vec<i32> {
1026    fn axes(self) -> Vec<i32> {
1027        self
1028    }
1029}
1030
1031macro_rules! axes_tuples {
1032    ($(($($t:ident $i:tt),+)),*) => {$(
1033        impl<$($t: InAxis),+> InAxes for ($($t,)+) {
1034            fn axes(self) -> Vec<Option<i32>> {
1035                vec![$(self.$i.axis()),+]
1036            }
1037        }
1038        impl OutAxes for ($(axes_tuples!(@i32 $t),)+) {
1039            fn axes(self) -> Vec<i32> {
1040                vec![$(self.$i),+]
1041            }
1042        }
1043    )*};
1044    (@i32 $t:ident) => { i32 };
1045}
1046
1047axes_tuples!(
1048    (A 0, B 1),
1049    (A 0, B 1, C 2),
1050    (A 0, B 1, C 2, D 3),
1051    (A 0, B 1, C 2, D 3, E 4),
1052    (A 0, B 1, C 2, D 3, E 4, F 5),
1053    (A 0, B 1, C 2, D 3, E 4, F 5, G 6),
1054    (A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7)
1055);
1056
1057/// The top-level argument of each leaf: the index of its path's first segment among the
1058/// distinct first segments, in order; and how many there are.
1059fn top_level(paths: &[String]) -> (Vec<usize>, usize) {
1060    let mut firsts: Vec<&str> = Vec::new();
1061    let groups = paths
1062        .iter()
1063        .map(|p| {
1064            let first = p.split('.').next().unwrap_or("");
1065            match firsts.iter().position(|f| *f == first) {
1066                Some(i) => i,
1067                None => {
1068                    firsts.push(first);
1069                    firsts.len() - 1
1070                }
1071            }
1072        })
1073        .collect();
1074    (groups, firsts.len())
1075}
1076
1077/// A function vectorized over batch axes ([`vmap`]): configure it with
1078/// [`Vmap::in_axes`]/[`Vmap::out_axes`], then call it with [`Vmap::call`] or turn it into a
1079/// closure with [`Vmap::into_fn`] (to compose with `compile` and the other transforms).
1080pub struct Vmap<I, O, F> {
1081    f: F,
1082    in_axes: Vec<Option<i32>>,
1083    out_axes: Vec<i32>,
1084    types: PhantomData<fn(&I) -> O>,
1085}
1086
1087/// Returns `f` vectorized over a batch axis of its inputs (axis 0 of every top-level
1088/// argument and output by default; see [`Vmap::in_axes`] and [`Vmap::out_axes`]).
1089///
1090/// `f` takes a tree (an array, a tuple of arguments, a struct) and returns one. Outputs
1091/// that do not depend on a batched input are broadcast along the batch axis. Arrays cannot
1092/// be evaluated inside `f`.
1093///
1094/// ```
1095/// use nuvec_core::{Array, Result, array, vmap};
1096///
1097/// # fn main() -> Result<()> {
1098/// // A matrix-vector product per example, sharing the matrix.
1099/// let mv = vmap(|(m, v): &(Array, Array)| m.matmul(v)).in_axes((None, 0));
1100/// let m = array![[1.0, 0.0], [0.0, 2.0]];
1101/// let vs = array![[1.0, 1.0], [3.0, 4.0]]; // two vectors
1102/// assert_eq!(mv.call(&(m, vs))?.to_vec::<f32>()?, [1.0, 2.0, 3.0, 8.0]);
1103/// # Ok(())
1104/// # }
1105/// ```
1106pub fn vmap<I, O, F, R>(f: F) -> Vmap<I, O, F>
1107where
1108    I: Tree,
1109    O: Tree,
1110    F: Fn(&I) -> R,
1111    R: IntoResult<Ok = O>,
1112{
1113    Vmap {
1114        f,
1115        in_axes: vec![Some(0)],
1116        out_axes: vec![0],
1117        types: PhantomData,
1118    }
1119}
1120
1121impl<I: Tree, O: Tree, F> Vmap<I, O, F> {
1122    /// The batch axis of each top-level argument (`None`: shared by every example), or one
1123    /// axis for all: `in_axes((None, 0))`, `in_axes(1)`. Negative axes count from the end.
1124    pub fn in_axes(mut self, axes: impl InAxes) -> Self {
1125        self.in_axes = axes.axes();
1126        self
1127    }
1128
1129    /// The axis where each top-level output gets the batch dimension, or one for all.
1130    pub fn out_axes(mut self, axes: impl OutAxes) -> Self {
1131        self.out_axes = axes.axes();
1132        self
1133    }
1134
1135    /// `f` batched over `inputs`.
1136    pub fn call<R>(&self, inputs: &I) -> Result<O>
1137    where
1138        F: Fn(&I) -> R,
1139        R: IntoResult<Ok = O>,
1140    {
1141        let named = inputs.flatten();
1142        let paths: Vec<String> = named.iter().map(|(p, _)| p.clone()).collect();
1143        let args: Vec<Array> = named.into_iter().map(|(_, a)| a).collect();
1144        let (groups, n) = top_level(&paths);
1145        let per_arg = |k: usize| match self.in_axes.as_slice() {
1146            [one] => Ok(*one),
1147            axes if axes.len() == n => Ok(axes[groups[k]]),
1148            axes => Err(Error::InvalidArgument(format!(
1149                "vmap: {} input axes for {n} top-level arguments",
1150                axes.len()
1151            ))),
1152        };
1153        let axes = args
1154            .iter()
1155            .enumerate()
1156            .map(|(k, a)| {
1157                per_arg(k)?
1158                    .map(|axis| normalize_axis(axis as isize, a.ndim()))
1159                    .transpose()
1160                    .map_err(|e| match e {
1161                        Error::InvalidArgument(m) => {
1162                            Error::InvalidArgument(format!("{m} (argument {:?})", paths[k]))
1163                        }
1164                        e => e,
1165                    })
1166            })
1167            .collect::<Result<Vec<_>>>()?;
1168        let template: RefCell<Option<O>> = RefCell::new(None);
1169        let body = |xs: &[Array]| -> Result<Vec<Array>> {
1170            let out = (self.f)(&inputs.unflatten(xs.iter().cloned())?).into_result()?;
1171            let leaves = out.leaves();
1172            *template.borrow_mut() = Some(out);
1173            Ok(leaves)
1174        };
1175        let outputs = batch(&body, &args, &axes)?;
1176        let template = template
1177            .into_inner()
1178            .ok_or_else(|| Error::InvalidArgument("vmap: the function did not run".into()))?;
1179        let out_paths = template.paths();
1180        let (out_groups, m) = top_level(&out_paths);
1181        let moved = outputs
1182            .iter()
1183            .enumerate()
1184            .map(|(k, o)| {
1185                let axis = match self.out_axes.as_slice() {
1186                    [one] => *one,
1187                    axes if axes.len() == m => axes[out_groups[k]],
1188                    axes => {
1189                        return Err(Error::InvalidArgument(format!(
1190                            "vmap: {} output axes for {m} top-level outputs",
1191                            axes.len()
1192                        )));
1193                    }
1194                };
1195                let axis = normalize_axis(axis as isize, o.ndim())?;
1196                crate::batching::move_axis(o, 0, axis)
1197            })
1198            .collect::<Result<Vec<_>>>()?;
1199        template.unflatten(moved)
1200    }
1201
1202    /// The configured batched function as a closure, which composes with `compile`, `grad`
1203    /// and the other transforms.
1204    pub fn into_fn<R>(self) -> impl Fn(&I) -> Result<O>
1205    where
1206        F: Fn(&I) -> R,
1207        R: IntoResult<Ok = O>,
1208    {
1209        move |inputs| self.call(inputs)
1210    }
1211}