Skip to main content

nuvec_core/
custom.rs

1//! Functions with custom transform rules ([`custom_function`]) and gradient
2//! checkpointing ([`checkpoint`]), over trees and (the `*_flat` forms) positional arrays.
3
4use std::fmt;
5use std::hash::{Hash, Hasher};
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex, PoisonError};
8
9use crate::primitive::Primitive;
10use crate::transforms::IntoResult;
11use crate::tree::Tree;
12use crate::{Array, Error, Result};
13
14type Body = dyn Fn(&[Array]) -> Result<Vec<Array>> + Send + Sync;
15type VjpRule = dyn Fn(&[Array], &[Array], &[Array]) -> Result<Vec<Array>> + Send + Sync;
16type JvpRule = dyn Fn(&[Array], &[Array]) -> Result<Vec<Array>> + Send + Sync;
17type VmapRule = dyn Fn(&[Array], &[Option<usize>]) -> Result<(Vec<Array>, usize)> + Send + Sync;
18
19/// A function and its optional custom rules.
20#[derive(Clone)]
21pub(crate) struct Rules {
22    pub(crate) body: Arc<Body>,
23    pub(crate) vjp: Option<Arc<VjpRule>>,
24    pub(crate) jvp: Option<Arc<JvpRule>>,
25    pub(crate) vmap: Option<Arc<VmapRule>>,
26}
27
28/// One call of a custom function, as recorded in the graph: its rules and the number of
29/// primal inputs. Equal (and hashed) by the identity of the function.
30#[derive(Clone)]
31pub struct CustomCall {
32    pub(crate) rules: Arc<Rules>,
33    pub(crate) primals: usize,
34}
35
36impl PartialEq for CustomCall {
37    fn eq(&self, other: &Self) -> bool {
38        Arc::ptr_eq(&self.rules, &other.rules) && self.primals == other.primals
39    }
40}
41
42impl Eq for CustomCall {}
43
44impl Hash for CustomCall {
45    fn hash<H: Hasher>(&self, state: &mut H) {
46        (Arc::as_ptr(&self.rules) as *const () as usize).hash(state);
47        self.primals.hash(state);
48    }
49}
50
51impl fmt::Debug for CustomCall {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "CustomCall({} primals)", self.primals)
54    }
55}
56
57/// A function of positional arrays with custom transform rules; see
58/// [`custom_function_flat`].
59#[derive(Clone)]
60pub struct CustomFunctionFlat {
61    rules: Arc<Rules>,
62}
63
64/// Wraps `f` (a function of positional arrays) so that transforms use custom rules for it,
65/// set with [`CustomFunctionFlat::with_vjp`], [`CustomFunctionFlat::with_jvp`] and
66/// [`CustomFunctionFlat::with_vmap`]; the flat form of [`custom_function`].
67///
68/// Without a rule, a transform differentiates or batches `f` itself, re-running it. `f`'s
69/// intermediates are never kept for a transform, so a custom function without rules is a
70/// gradient checkpoint (see [`checkpoint_flat`]).
71///
72/// ```
73/// use nuvec_core::{Array, Result, array, custom_function_flat, grad};
74///
75/// # fn main() -> Result<()> {
76/// // A straight-through estimator: forward x², gradient as if the identity.
77/// let square = custom_function_flat(|xs| Ok(vec![xs[0].multiply(&xs[0])]))
78///     .with_vjp(|_primals, _outputs, cotangents| Ok(cotangents.to_vec()));
79/// let g = grad(move |x: &Array| Ok(square.call(std::slice::from_ref(x))?[0].sum(..)));
80/// let x = array![3.0, -1.0];
81/// assert_eq!(g(&x)?.to_vec::<f32>()?, [1.0, 1.0]);
82/// # Ok(())
83/// # }
84/// ```
85pub fn custom_function_flat(
86    f: impl Fn(&[Array]) -> Result<Vec<Array>> + Send + Sync + 'static,
87) -> CustomFunctionFlat {
88    CustomFunctionFlat {
89        rules: Arc::new(Rules {
90            body: Arc::new(f),
91            vjp: None,
92            jvp: None,
93            vmap: None,
94        }),
95    }
96}
97
98impl CustomFunctionFlat {
99    fn with(self, set: impl FnOnce(&mut Rules)) -> Self {
100        let mut rules = (*self.rules).clone();
101        set(&mut rules);
102        Self {
103            rules: Arc::new(rules),
104        }
105    }
106
107    /// Sets the reverse-mode rule: `(primals, outputs, cotangents)` → one cotangent per
108    /// primal.
109    pub fn with_vjp(
110        self,
111        rule: impl Fn(&[Array], &[Array], &[Array]) -> Result<Vec<Array>> + Send + Sync + 'static,
112    ) -> Self {
113        self.with(|r| r.vjp = Some(Arc::new(rule)))
114    }
115
116    /// Sets the forward-mode rule: `(primals, tangents)` → one tangent per output (missing
117    /// input tangents are passed as zeros).
118    pub fn with_jvp(
119        self,
120        rule: impl Fn(&[Array], &[Array]) -> Result<Vec<Array>> + Send + Sync + 'static,
121    ) -> Self {
122        self.with(|r| r.jvp = Some(Arc::new(rule)))
123    }
124
125    /// Sets the batching rule: `(inputs, batch axes)` (`None` for unbatched inputs) →
126    /// the batched outputs and their common batch axis.
127    pub fn with_vmap(
128        self,
129        rule: impl Fn(&[Array], &[Option<i32>]) -> Result<(Vec<Array>, i32)> + Send + Sync + 'static,
130    ) -> Self {
131        let rule = move |xs: &[Array], axes: &[Option<usize>]| {
132            let axes: Vec<Option<i32>> = axes
133                .iter()
134                .map(|a| a.map(|a| i32::try_from(a).unwrap_or(i32::MAX)))
135                .collect();
136            let (out, axis) = rule(xs, &axes)?;
137            let axis = usize::try_from(axis).map_err(|_| {
138                Error::InvalidArgument(format!(
139                    "custom_function: the vmap rule returned the negative output axis {axis}"
140                ))
141            })?;
142            Ok((out, axis))
143        };
144        self.with(|r| r.vmap = Some(Arc::new(rule)))
145    }
146
147    /// Calls the function on `inputs`. Values equal `f(inputs)`; transforms of the
148    /// results use the custom rules.
149    pub fn call(&self, inputs: &[Array]) -> Result<Vec<Array>> {
150        // `f` runs on untraced stop-gradient views: its graph detaches after evaluation
151        // even inside a transform, and transforms never walk into it.
152        let views = inputs
153            .iter()
154            .map(|x| Ok(Array::record(Primitive::StopGradient, vec![x.clone()], None)?.remove(0)))
155            .collect::<Result<Vec<_>>>()?;
156        let outputs = (self.rules.body)(&views)?;
157        let trace = crate::transforms::inherited(inputs);
158        let call = CustomCall {
159            rules: self.rules.clone(),
160            primals: inputs.len(),
161        };
162        let all = inputs.iter().chain(&outputs).cloned().collect();
163        Array::record(Primitive::Custom(call), all, trace)
164    }
165}
166
167/// Returns `f` (a function of positional arrays) with gradient checkpointing; the flat form
168/// of [`checkpoint`].
169///
170/// ```
171/// use nuvec_core::{Array, Result, checkpoint_flat, grad};
172///
173/// # fn main() -> Result<()> {
174/// let block = checkpoint_flat(|xs| Ok(vec![xs[0].sin().sin()]));
175/// let g = grad(move |x: &Array| Ok(block(std::slice::from_ref(x))?[0].sum(..)));
176/// let x = Array::from(0.5f32);
177/// let expect = 0.5f32.sin().cos() * 0.5f32.cos();
178/// assert!((g(&x)?.item::<f32>()? - expect).abs() < 1e-6);
179/// # Ok(())
180/// # }
181/// ```
182pub fn checkpoint_flat(
183    f: impl Fn(&[Array]) -> Result<Vec<Array>> + Send + Sync + 'static,
184) -> impl Fn(&[Array]) -> Result<Vec<Array>> + Send + Sync {
185    let function = custom_function_flat(f);
186    move |inputs| function.call(inputs)
187}
188
189// ── Over trees ──────────────────────────────────────────────────────────────
190
191type TreeBody<I, O> = dyn Fn(&I) -> Result<O> + Send + Sync;
192type TreeVjp<I, O> = dyn Fn(&I, &O, &O) -> Result<I> + Send + Sync;
193type TreeJvp<I, O> = dyn Fn(&I, &I) -> Result<O> + Send + Sync;
194type TreeVmap<I, O> = dyn Fn(&I, &[Option<i32>]) -> Result<(O, i32)> + Send + Sync;
195
196/// A function of a tree with custom transform rules; see [`custom_function`].
197pub struct CustomFunction<I, O> {
198    body: Arc<TreeBody<I, O>>,
199    vjp: Option<Arc<TreeVjp<I, O>>>,
200    jvp: Option<Arc<TreeJvp<I, O>>>,
201    vmap: Option<Arc<TreeVmap<I, O>>>,
202    /// Restore the thread's random key around recomputations (checkpoints).
203    replay_random: bool,
204}
205
206impl<I, O> Clone for CustomFunction<I, O> {
207    fn clone(&self) -> Self {
208        Self {
209            body: self.body.clone(),
210            vjp: self.vjp.clone(),
211            jvp: self.jvp.clone(),
212            vmap: self.vmap.clone(),
213            replay_random: self.replay_random,
214        }
215    }
216}
217
218/// Wraps `f` (a function of a tree, returning a tree) so that transforms use custom rules
219/// for it, set with [`CustomFunction::with_vjp`], [`CustomFunction::with_jvp`] and
220/// [`CustomFunction::with_vmap`].
221///
222/// Without a rule, a transform differentiates or batches `f` itself, re-running it; `f`'s
223/// intermediates are never kept for a transform. The rules are stored in the graph (and
224/// may run later, on another thread), so `f` and the rules are `Send + Sync + 'static`.
225///
226/// ```
227/// use nuvec_core::{Array, Result, array, custom_function, grad};
228///
229/// # fn main() -> Result<()> {
230/// // A straight-through estimator: forward x², gradient as if the identity.
231/// let square = custom_function(|x: &Array| x.multiply(x))
232///     .with_vjp(|_x: &Array, _y: &Array, cotangent: &Array| Ok(cotangent.clone()));
233/// let g = grad(move |x: &Array| Ok(square.call(x)?.sum(..)));
234/// assert_eq!(g(&array![3.0, -1.0])?.to_vec::<f32>()?, [1.0, 1.0]);
235/// # Ok(())
236/// # }
237/// ```
238pub fn custom_function<I, O, F, R>(f: F) -> CustomFunction<I, O>
239where
240    I: Tree + Send + Sync,
241    O: Tree + Send + Sync,
242    F: Fn(&I) -> R + Send + Sync + 'static,
243    R: IntoResult<Ok = O>,
244{
245    CustomFunction {
246        body: Arc::new(move |i: &I| f(i).into_result()),
247        vjp: None,
248        jvp: None,
249        vmap: None,
250        replay_random: false,
251    }
252}
253
254impl<I, O> CustomFunction<I, O>
255where
256    I: Tree + Send + Sync,
257    O: Tree + Send + Sync,
258{
259    /// Sets the reverse-mode rule: `(primals, outputs, cotangents)` → the cotangent of the
260    /// primals (a tree like them).
261    pub fn with_vjp(
262        mut self,
263        rule: impl Fn(&I, &O, &O) -> Result<I> + Send + Sync + 'static,
264    ) -> Self {
265        self.vjp = Some(Arc::new(rule));
266        self
267    }
268
269    /// Sets the forward-mode rule: `(primals, tangents)` → the outputs' tangents (missing
270    /// input tangents are passed as zeros).
271    pub fn with_jvp(mut self, rule: impl Fn(&I, &I) -> Result<O> + Send + Sync + 'static) -> Self {
272        self.jvp = Some(Arc::new(rule));
273        self
274    }
275
276    /// Sets the batching rule: `(inputs, batch axis of each input leaf)` (`None` for
277    /// unbatched leaves) → the batched outputs and their common batch axis.
278    pub fn with_vmap(
279        mut self,
280        rule: impl Fn(&I, &[Option<i32>]) -> Result<(O, i32)> + Send + Sync + 'static,
281    ) -> Self {
282        self.vmap = Some(Arc::new(rule));
283        self
284    }
285
286    /// Calls the function on `inputs`. Values equal `f(inputs)`; transforms of the results
287    /// use the custom rules.
288    pub fn call(&self, inputs: &I) -> Result<O> {
289        let template = Arc::new(inputs.clone());
290        let output: Arc<Mutex<Option<O>>> = Arc::new(Mutex::new(None));
291        let out_template = {
292            let output = output.clone();
293            move || {
294                output
295                    .lock()
296                    .unwrap_or_else(PoisonError::into_inner)
297                    .clone()
298                    .ok_or_else(|| Error::InvalidArgument("custom_function: no output yet".into()))
299            }
300        };
301        let body = {
302            let (f, template, output) = (self.body.clone(), template.clone(), output.clone());
303            // A checkpoint's recomputation draws the same random numbers as its forward
304            // pass: the thread's key is restored around every run after the first.
305            let key = self.replay_random.then(crate::random::sequence_key);
306            let first = AtomicBool::new(true);
307            move |xs: &[Array]| -> Result<Vec<Array>> {
308                let restore = match &key {
309                    Some(k) => {
310                        let current = crate::random::sequence_key();
311                        crate::random::set_sequence_key(k.clone());
312                        (!first.swap(false, Ordering::AcqRel)).then_some(current)
313                    }
314                    None => None,
315                };
316                let out = template.unflatten(xs.iter().cloned()).and_then(|i| f(&i));
317                if let Some(current) = restore {
318                    crate::random::set_sequence_key(current);
319                }
320                let out = out?;
321                let leaves = out.leaves();
322                *output.lock().unwrap_or_else(PoisonError::into_inner) = Some(out);
323                Ok(leaves)
324            }
325        };
326        let vjp: Option<Arc<VjpRule>> = self.vjp.clone().map(|rule| {
327            let (template, out_template) = (template.clone(), out_template.clone());
328            Arc::new(move |p: &[Array], o: &[Array], c: &[Array]| {
329                let tpl = out_template()?;
330                let i = template.unflatten(p.iter().cloned())?;
331                let o = tpl.unflatten(o.iter().cloned())?;
332                let c = tpl.unflatten(c.iter().cloned())?;
333                Ok(rule(&i, &o, &c)?.leaves())
334            }) as Arc<VjpRule>
335        });
336        let jvp: Option<Arc<JvpRule>> = self.jvp.clone().map(|rule| {
337            let template = template.clone();
338            Arc::new(move |p: &[Array], t: &[Array]| {
339                let i = template.unflatten(p.iter().cloned())?;
340                let t = template.unflatten(t.iter().cloned())?;
341                Ok(rule(&i, &t)?.leaves())
342            }) as Arc<JvpRule>
343        });
344        let vmap: Option<Arc<VmapRule>> = self.vmap.clone().map(|rule| {
345            let (template, out_template) = (template.clone(), out_template.clone());
346            Arc::new(move |xs: &[Array], axes: &[Option<usize>]| {
347                let axes: Vec<Option<i32>> = axes
348                    .iter()
349                    .map(|a| a.map(|a| i32::try_from(a).unwrap_or(i32::MAX)))
350                    .collect();
351                let i = template.unflatten(xs.iter().cloned())?;
352                let (o, axis) = rule(&i, &axes)?;
353                let axis = usize::try_from(axis).map_err(|_| {
354                    Error::InvalidArgument(format!(
355                        "custom_function: the vmap rule returned the negative output axis {axis}"
356                    ))
357                })?;
358                let _ = out_template()?;
359                Ok((o.leaves(), axis))
360            }) as Arc<VmapRule>
361        });
362        let flat = CustomFunctionFlat {
363            rules: Arc::new(Rules {
364                body: Arc::new(body),
365                vjp,
366                jvp,
367                vmap,
368            }),
369        };
370        let leaves = flat.call(&inputs.leaves())?;
371        out_template()?.unflatten(leaves)
372    }
373}
374
375/// Returns `f` with gradient checkpointing: values and derivatives equal `f`'s, but `f`'s
376/// intermediates are not kept for the backward pass; `f` runs again to differentiate.
377/// Random numbers drawn inside `f` (dropout) are drawn again with the same key, so the
378/// recomputation sees the same masks.
379///
380/// `f` is stored in the graph for the recomputation, so it is `Send + Sync + 'static`
381/// (it cannot borrow locals: move them in).
382///
383/// ```
384/// use nuvec_core::{Array, Result, checkpoint, grad};
385///
386/// # fn main() -> Result<()> {
387/// let block = checkpoint(|x: &Array| x.sin().sin());
388/// let g = grad(move |x: &Array| Ok(block(x)?.sum(..)));
389/// let x = Array::from(0.5f32);
390/// let expect = 0.5f32.sin().cos() * 0.5f32.cos();
391/// assert!((g(&x)?.item::<f32>()? - expect).abs() < 1e-6);
392/// # Ok(())
393/// # }
394/// ```
395pub fn checkpoint<I, O, F, R>(f: F) -> impl Fn(&I) -> Result<O> + Send + Sync
396where
397    I: Tree + Send + Sync,
398    O: Tree + Send + Sync,
399    F: Fn(&I) -> R + Send + Sync + 'static,
400    R: IntoResult<Ok = O>,
401{
402    let mut function = custom_function(f);
403    function.replay_random = true;
404    move |inputs| function.call(inputs)
405}
406
407#[cfg(test)]
408mod tests {
409    use std::sync::Mutex;
410
411    use super::*;
412    use crate::array::State;
413
414    /// Whether `f`'s forward intermediate keeps its graph after `grad` evaluates the
415    /// forward pass inside the transform.
416    fn intermediate_retained(checkpointed: bool) -> bool {
417        let first = Arc::new(Mutex::new(None::<Array>));
418        let stored = first.clone();
419        let body = move |xs: &[Array]| {
420            let h = crate::ops::imp::sin(&xs[0])?;
421            stored.lock().unwrap().get_or_insert_with(|| h.clone());
422            Ok(vec![crate::ops::imp::sin(&h)?])
423        };
424        let f: Box<Body> = if checkpointed {
425            Box::new(checkpoint_flat(body))
426        } else {
427            Box::new(body)
428        };
429        // Checked inside the transform: retained nodes detach when it exits.
430        let retained = Arc::new(Mutex::new(false));
431        let seen = retained.clone();
432        let g = crate::grad(move |x: &Array| {
433            let y = f(std::slice::from_ref(x))?.remove(0);
434            y.eval()?;
435            let h = first.lock().unwrap().take().unwrap();
436            *seen.lock().unwrap() = matches!(&*h.node.lock(), State::Retained { .. });
437            crate::ops::imp::sum(&y, &[], false)
438        });
439        let _ = g(&Array::from(0.5f32)).unwrap();
440        *retained.lock().unwrap()
441    }
442
443    #[test]
444    fn checkpoint_does_not_keep_intermediates() {
445        assert!(
446            intermediate_retained(false),
447            "without checkpoint the graph is kept"
448        );
449        assert!(
450            !intermediate_retained(true),
451            "with checkpoint it is released"
452        );
453    }
454}