use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use crate::primitive::Primitive;
use crate::{Array, Result};
type Body = dyn Fn(&[Array]) -> Result<Vec<Array>> + Send + Sync;
type VjpRule = dyn Fn(&[Array], &[Array], &[Array]) -> Result<Vec<Array>> + Send + Sync;
type JvpRule = dyn Fn(&[Array], &[Array]) -> Result<Vec<Array>> + Send + Sync;
type VmapRule = dyn Fn(&[Array], &[Option<usize>]) -> Result<(Vec<Array>, usize)> + Send + Sync;
#[derive(Clone)]
pub(crate) struct Rules {
pub(crate) body: Arc<Body>,
pub(crate) vjp: Option<Arc<VjpRule>>,
pub(crate) jvp: Option<Arc<JvpRule>>,
pub(crate) vmap: Option<Arc<VmapRule>>,
}
#[derive(Clone)]
pub struct CustomCall {
pub(crate) rules: Arc<Rules>,
pub(crate) primals: usize,
}
impl PartialEq for CustomCall {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.rules, &other.rules) && self.primals == other.primals
}
}
impl Eq for CustomCall {}
impl Hash for CustomCall {
fn hash<H: Hasher>(&self, state: &mut H) {
(Arc::as_ptr(&self.rules) as *const () as usize).hash(state);
self.primals.hash(state);
}
}
impl fmt::Debug for CustomCall {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CustomCall({} primals)", self.primals)
}
}
#[derive(Clone)]
pub struct CustomFunction {
rules: Arc<Rules>,
}
pub fn custom_function(
f: impl Fn(&[Array]) -> Result<Vec<Array>> + Send + Sync + 'static,
) -> CustomFunction {
CustomFunction {
rules: Arc::new(Rules {
body: Arc::new(f),
vjp: None,
jvp: None,
vmap: None,
}),
}
}
impl CustomFunction {
fn with(self, set: impl FnOnce(&mut Rules)) -> Self {
let mut rules = (*self.rules).clone();
set(&mut rules);
Self {
rules: Arc::new(rules),
}
}
pub fn with_vjp(
self,
rule: impl Fn(&[Array], &[Array], &[Array]) -> Result<Vec<Array>> + Send + Sync + 'static,
) -> Self {
self.with(|r| r.vjp = Some(Arc::new(rule)))
}
pub fn with_jvp(
self,
rule: impl Fn(&[Array], &[Array]) -> Result<Vec<Array>> + Send + Sync + 'static,
) -> Self {
self.with(|r| r.jvp = Some(Arc::new(rule)))
}
pub fn with_vmap(
self,
rule: impl Fn(&[Array], &[Option<usize>]) -> Result<(Vec<Array>, usize)> + Send + Sync + 'static,
) -> Self {
self.with(|r| r.vmap = Some(Arc::new(rule)))
}
pub fn call(&self, inputs: &[Array]) -> Result<Vec<Array>> {
let views = inputs
.iter()
.map(|x| Ok(Array::record(Primitive::StopGradient, vec![x.clone()], false)?.remove(0)))
.collect::<Result<Vec<_>>>()?;
let outputs = (self.rules.body)(&views)?;
let tracer = inputs.iter().any(|a| a.node.tracer);
let call = CustomCall {
rules: self.rules.clone(),
primals: inputs.len(),
};
let all = inputs.iter().chain(&outputs).cloned().collect();
Array::record(Primitive::Custom(call), all, tracer)
}
}
pub fn checkpoint(
f: impl Fn(&[Array]) -> Result<Vec<Array>> + Send + Sync + 'static,
) -> impl Fn(&[Array]) -> Result<Vec<Array>> + Send + Sync {
let function = custom_function(f);
move |inputs| function.call(inputs)
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
use crate::array::State;
fn intermediate_retained(checkpointed: bool) -> bool {
let first = Arc::new(Mutex::new(None::<Array>));
let stored = first.clone();
let body = move |xs: &[Array]| {
let h = crate::sin(&xs[0])?;
stored.lock().unwrap().get_or_insert_with(|| h.clone());
Ok(vec![crate::sin(&h)?])
};
let f: Box<Body> = if checkpointed {
Box::new(checkpoint(body))
} else {
Box::new(body)
};
let g = crate::grad(move |x: &Array| {
let y = f(std::slice::from_ref(x))?.remove(0);
y.eval()?;
crate::sum(&y, &[], false)
});
g(&Array::scalar(0.5f32)).unwrap();
let h = first.lock().unwrap().take().unwrap();
let retained = matches!(&*h.node.lock(), State::Retained { .. });
retained
}
#[test]
fn checkpoint_does_not_keep_intermediates() {
assert!(
intermediate_retained(false),
"without checkpoint the graph is kept"
);
assert!(
!intermediate_retained(true),
"with checkpoint it is released"
);
}
}