use std::cell::Cell;
use std::collections::HashSet;
use std::sync::Arc;
use crate::array::{Node, NodeMap, NodeSet, State};
use crate::cast::Canon;
use crate::derivatives::zeros_like;
use crate::primitive::Primitive;
use crate::{Array, Error, Result, ops};
thread_local!(static DEPTH: Cell<usize> = const { Cell::new(0) });
pub(crate) fn tracing() -> bool {
DEPTH.get() > 0
}
struct Tracing;
impl Tracing {
fn enter() -> Self {
DEPTH.set(DEPTH.get() + 1);
Tracing
}
}
impl Drop for Tracing {
fn drop(&mut self) {
DEPTH.set(DEPTH.get() - 1);
}
}
fn tracer(primal: &Array) -> Result<Array> {
let shape = primal.shape().to_vec();
Ok(Array::record(Primitive::Broadcast(shape), vec![primal.clone()], true)?.swap_remove(0))
}
type Key = (*const Node, u32);
fn key(a: &Array) -> Key {
(Arc::as_ptr(&a.node), a.output)
}
struct Step {
node: Arc<Node>,
primitive: Primitive,
inputs: Vec<Array>,
wanted: Vec<bool>,
}
impl Step {
fn outputs(&self) -> Vec<Array> {
(0..self.node.outputs.len() as u32)
.map(|output| Array {
node: self.node.clone(),
output,
})
.collect()
}
}
type Graph = (Primitive, Vec<Array>);
fn graph(node: &Node) -> Option<Graph> {
match &*node.lock() {
State::Pending { primitive, inputs } | State::Retained { primitive, inputs } => {
Some((primitive.clone(), inputs.clone()))
}
State::Done => None,
}
}
fn followed(primitive: &Primitive, inputs: &[Array]) -> usize {
match primitive {
Primitive::Custom(call) => call.primals,
_ => inputs.len(),
}
}
fn tape(outputs: &[Array], sources: &NodeSet, stop_gradient: bool) -> Vec<Step> {
let mut depends: NodeMap<*const Node, bool> = NodeMap::default();
let mut visited = NodeSet::default();
let mut order = Vec::new();
let mut stack: Vec<(Arc<Node>, Option<Graph>)> =
outputs.iter().map(|a| (a.node.clone(), None)).collect();
while let Some((node, expanded)) = stack.pop() {
let ptr = Arc::as_ptr(&node);
if let Some((primitive, inputs)) = expanded {
let n = followed(&primitive, &inputs);
let wanted: Vec<bool> = inputs
.iter()
.enumerate()
.map(|(i, a)| i < n && depends.get(&Arc::as_ptr(&a.node)).copied().unwrap_or(false))
.collect();
let cut = stop_gradient && primitive == Primitive::StopGradient;
let dep = !cut && wanted.contains(&true);
depends.insert(ptr, dep);
if dep {
order.push(Step {
node,
primitive,
inputs,
wanted,
});
}
continue;
}
if !visited.insert(ptr) {
continue;
}
if sources.contains(&ptr) {
depends.insert(ptr, true);
continue;
}
let Some((primitive, inputs)) = graph(&node) else {
continue; };
let next: Vec<Arc<Node>> = inputs[..followed(&primitive, &inputs)]
.iter()
.map(|a| a.node.clone())
.filter(|n| !visited.contains(&Arc::as_ptr(n)))
.collect();
stack.push((node, Some((primitive, inputs))));
stack.extend(next.into_iter().map(|n| (n, None)));
}
order
}
fn accumulate(map: &mut NodeMap<Key, Array>, key: Key, value: Array) -> Result<()> {
let value = match map.remove(&key) {
Some(previous) => ops::add(&previous, &value)?,
None => value,
};
map.insert(key, value);
Ok(())
}
fn check_shapes(op: &str, what: &str, arrays: &[Array], given: &[Array]) -> Result<()> {
if arrays.len() != given.len() {
return Err(Error::InvalidArgument(format!(
"{op}: {} {what}s for {} arrays",
given.len(),
arrays.len()
)));
}
for (a, g) in arrays.iter().zip(given) {
if a.shape() != g.shape() {
return Err(Error::InvalidArgument(format!(
"{op}: shape {:?} does not match {what} shape {:?}",
a.shape(),
g.shape()
)));
}
}
Ok(())
}
fn reverse(outputs: &[Array], cotangents: &[Array], tracers: &[Array]) -> Result<Vec<Array>> {
let sources = tracers.iter().map(|t| Arc::as_ptr(&t.node)).collect();
let mut cot = NodeMap::default();
for (o, c) in outputs.iter().zip(cotangents) {
accumulate(&mut cot, key(o), c.clone())?;
}
for step in tape(outputs, &sources, true).iter().rev() {
let outs = step.outputs();
if !outs.iter().any(|o| cot.contains_key(&key(o))) {
continue;
}
let cots = outs
.iter()
.map(|o| cot.remove(&key(o)).map_or_else(|| zeros_like(o), Ok))
.collect::<Result<Vec<_>>>()?;
let grads = step
.primitive
.vjp(&step.inputs, &outs, &cots, &step.wanted)?;
for ((input, grad), &wanted) in step.inputs.iter().zip(grads).zip(&step.wanted) {
if let (Some(grad), true) = (grad, wanted) {
accumulate(&mut cot, key(input), grad)?;
}
}
}
tracers
.iter()
.map(|t| cot.remove(&key(t)).map_or_else(|| zeros_like(t), Ok))
.collect()
}
pub fn vjp(
f: impl FnOnce(&[Array]) -> Result<Vec<Array>>,
primals: &[Array],
cotangents: &[Array],
) -> Result<(Vec<Array>, Vec<Array>)> {
let _tracing = Tracing::enter();
let tracers = primals.iter().map(tracer).collect::<Result<Vec<_>>>()?;
let outputs = f(&tracers)?;
check_shapes("vjp", "cotangent", &outputs, cotangents)?;
let grads = reverse(&outputs, cotangents, &tracers)?;
Ok((outputs, grads))
}
pub fn jvp(
f: impl FnOnce(&[Array]) -> Result<Vec<Array>>,
primals: &[Array],
tangents: &[Array],
) -> Result<(Vec<Array>, Vec<Array>)> {
check_shapes("jvp", "tangent", primals, tangents)?;
let _tracing = Tracing::enter();
let tracers = primals.iter().map(tracer).collect::<Result<Vec<_>>>()?;
let outputs = f(&tracers)?;
let sources = tracers.iter().map(|t| Arc::as_ptr(&t.node)).collect();
let mut tan: NodeMap<Key, Array> = tracers
.iter()
.zip(tangents)
.map(|(t, v)| (key(t), v.clone()))
.collect();
for step in tape(&outputs, &sources, true) {
let inputs: Vec<Option<Array>> = step
.inputs
.iter()
.map(|a| tan.get(&key(a)).cloned())
.collect();
if inputs.iter().all(Option::is_none) {
continue;
}
let outs = step.outputs();
for (o, t) in outs
.iter()
.zip(step.primitive.jvp(&step.inputs, &outs, &inputs)?)
{
tan.insert(key(o), t);
}
}
let tangents = outputs
.iter()
.map(|o| tan.get(&key(o)).cloned().map_or_else(|| zeros_like(o), Ok))
.collect::<Result<Vec<_>>>()?;
Ok((outputs, tangents))
}
pub fn value_and_grad<F>(
f: F,
argnums: &[usize],
) -> impl Fn(&[Array]) -> Result<(Array, Vec<Array>)>
where
F: Fn(&[Array]) -> Result<Array>,
{
let argnums = argnums.to_vec();
move |args| {
if argnums.is_empty() {
return Err(Error::InvalidArgument(
"value_and_grad: no argument to differentiate".into(),
));
}
let mut seen = HashSet::new();
for &i in &argnums {
if i >= args.len() || !seen.insert(i) {
return Err(Error::InvalidArgument(format!(
"value_and_grad: invalid or repeated argument {i} for {} arguments",
args.len()
)));
}
}
let _tracing = Tracing::enter();
let mut inputs = args.to_vec();
let mut tracers = Vec::with_capacity(argnums.len());
for &i in &argnums {
inputs[i] = tracer(&args[i])?;
tracers.push(inputs[i].clone());
}
let value = f(&inputs)?;
if value.ndim() != 0 {
return Err(Error::InvalidArgument(format!(
"value_and_grad: the function must return a scalar (0-D) array, got shape {:?}",
value.shape()
)));
}
let seed = ops::filled(&[], value.dtype(), |_| Canon::Int(1))?;
let grads = reverse(std::slice::from_ref(&value), &[seed], &tracers)?;
Ok((value, grads))
}
}
pub fn grad<F>(f: F) -> impl Fn(&Array) -> Result<Array>
where
F: Fn(&Array) -> Result<Array>,
{
let value_and_grad = value_and_grad(move |xs: &[Array]| f(&xs[0]), &[0]);
move |x| Ok(value_and_grad(std::slice::from_ref(x))?.1.swap_remove(0))
}
fn placeholder(like: &Array, shape: &[usize]) -> Result<Array> {
let failed = Array::failed(
Error::InvalidArgument("vmap: arrays cannot be evaluated inside vmap".into()),
like.dtype(),
&[],
);
let primitive = Primitive::Broadcast(shape.to_vec());
Ok(Array::record_unchecked(primitive, vec![failed], true)?.swap_remove(0))
}
pub(crate) fn batch(
f: &dyn Fn(&[Array]) -> Result<Vec<Array>>,
args: &[Array],
axes: &[Option<usize>],
) -> Result<Vec<Array>> {
let mut size = None;
for (a, axis) in args.iter().zip(axes) {
let Some(axis) = *axis else { continue };
let n = *a.shape().get(axis).ok_or_else(|| {
Error::InvalidArgument(format!(
"vmap: axis {axis} out of range for shape {:?}",
a.shape()
))
})?;
if size.is_some_and(|s| s != n) {
return Err(Error::InvalidArgument(format!(
"vmap: inconsistent batch sizes {} and {n}",
size.unwrap_or(0)
)));
}
size = Some(n);
}
let size = size
.ok_or_else(|| Error::InvalidArgument("vmap: at least one input must be batched".into()))?;
let _tracing = Tracing::enter();
let mut batched: NodeMap<Key, (Array, usize)> = NodeMap::default();
let mut sources = NodeSet::default();
let mut inputs = Vec::with_capacity(args.len());
for (a, axis) in args.iter().zip(axes) {
inputs.push(match *axis {
Some(axis) => {
let mut shape = a.shape().to_vec();
shape.remove(axis);
let p = placeholder(a, &shape)?;
sources.insert(Arc::as_ptr(&p.node));
batched.insert(key(&p), (a.clone(), axis));
p
}
None => a.clone(),
});
}
let outputs = f(&inputs)?;
for step in tape(&outputs, &sources, false) {
let (arrays, axes): (Vec<Array>, Vec<Option<usize>>) = step
.inputs
.iter()
.map(|a| match batched.get(&key(a)) {
Some((b, axis)) => (b.clone(), Some(*axis)),
None => (a.clone(), None),
})
.unzip();
let (results, axis) = step.primitive.vmap(&arrays, &axes)?;
for (o, r) in step.outputs().iter().zip(results) {
batched.insert(key(o), (r, axis));
}
}
outputs
.iter()
.map(|o| match batched.get(&key(o)) {
Some((b, axis)) => crate::batching::move_axis(b, *axis, 0),
None => {
let mut shape = vec![size];
shape.extend_from_slice(o.shape());
ops::broadcast_to(&ops::expand_dims(o, 0)?, &shape)
}
})
.collect()
}
fn normalize_axis(axis: isize, ndim: usize) -> Result<usize> {
let a = if axis < 0 { axis + ndim as isize } else { axis };
if (0..ndim as isize).contains(&a) {
Ok(a as usize)
} else {
Err(Error::InvalidArgument(format!(
"vmap: axis {axis} out of range for {ndim} dimensions"
)))
}
}
pub fn vmap<F>(
f: F,
in_axes: &[Option<isize>],
out_axes: &[isize],
) -> impl Fn(&[Array]) -> Result<Vec<Array>>
where
F: Fn(&[Array]) -> Result<Vec<Array>>,
{
let (in_axes, out_axes) = (in_axes.to_vec(), out_axes.to_vec());
move |args| {
let axes = if in_axes.is_empty() {
vec![Some(0); args.len()]
} else if in_axes.len() == args.len() {
in_axes
.iter()
.zip(args)
.map(|(axis, a)| axis.map(|axis| normalize_axis(axis, a.ndim())).transpose())
.collect::<Result<Vec<_>>>()?
} else {
return Err(Error::InvalidArgument(format!(
"vmap: {} input axes for {} arguments",
in_axes.len(),
args.len()
)));
};
let outputs = batch(&f, args, &axes)?;
if !out_axes.is_empty() && out_axes.len() != outputs.len() {
return Err(Error::InvalidArgument(format!(
"vmap: {} output axes for {} outputs",
out_axes.len(),
outputs.len()
)));
}
outputs
.iter()
.enumerate()
.map(|(i, o)| {
let axis = normalize_axis(out_axes.get(i).copied().unwrap_or(0), o.ndim())?;
crate::batching::move_axis(o, 0, axis)
})
.collect()
}
}