use crate::{NodeId, Op, SharedGraph, node_dtype, node_shape, push_node};
pub type GradientList = Vec<Option<NodeId>>;
pub trait GradientRule {
fn backward(
&self,
graph: &SharedGraph,
op: &Op,
inputs: &[NodeId],
output: NodeId,
upstream: NodeId,
) -> GradientList;
}
pub fn dispatch_gradient(
graph: &SharedGraph,
op: &Op,
inputs: &[NodeId],
output: NodeId,
upstream: NodeId,
) -> Option<GradientList> {
match op {
Op::Add => Some(AddRule.backward(graph, op, inputs, output, upstream)),
Op::Mul => Some(MulRule.backward(graph, op, inputs, output, upstream)),
Op::Relu => Some(ReluRule.backward(graph, op, inputs, output, upstream)),
Op::Equal | Op::Ne | Op::Lt | Op::Le | Op::Gt | Op::Ge => {
Some(NoGradientBinaryRule.backward(graph, op, inputs, output, upstream))
}
Op::Where => Some(WhereRule.backward(graph, op, inputs, output, upstream)),
_ => None,
}
}
pub struct AddRule;
impl GradientRule for AddRule {
fn backward(
&self,
_graph: &SharedGraph,
_op: &Op,
_inputs: &[NodeId],
_output: NodeId,
upstream: NodeId,
) -> GradientList {
vec![Some(upstream), Some(upstream)]
}
}
pub struct MulRule;
impl GradientRule for MulRule {
fn backward(
&self,
graph: &SharedGraph,
_op: &Op,
inputs: &[NodeId],
_output: NodeId,
upstream: NodeId,
) -> GradientList {
let a = inputs[0];
let b = inputs[1];
let a_shape = node_shape(graph, a);
let dtype = node_dtype(graph, a);
let grad_a = push_node(graph, Op::Mul, vec![upstream, b], a_shape.clone(), dtype);
let grad_b = push_node(graph, Op::Mul, vec![upstream, a], a_shape, dtype);
vec![Some(grad_a), Some(grad_b)]
}
}
pub struct NoGradientBinaryRule;
impl GradientRule for NoGradientBinaryRule {
fn backward(
&self,
_graph: &SharedGraph,
_op: &Op,
_inputs: &[NodeId],
_output: NodeId,
_upstream: NodeId,
) -> GradientList {
vec![None, None]
}
}
pub struct WhereRule;
impl GradientRule for WhereRule {
fn backward(
&self,
graph: &SharedGraph,
_op: &Op,
inputs: &[NodeId],
_output: NodeId,
upstream: NodeId,
) -> GradientList {
let cond = inputs[0];
let a = inputs[1];
let _b = inputs[2];
let shape = node_shape(graph, a);
let dtype = node_dtype(graph, a);
let m_a = push_node(graph, Op::Cast(dtype), vec![cond], shape.clone(), dtype);
let m_a_minus_one = push_node(graph, Op::AddScalar(-1.0), vec![m_a], shape.clone(), dtype);
let m_b = push_node(graph, Op::Neg, vec![m_a_minus_one], shape.clone(), dtype);
let grad_a = push_node(graph, Op::Mul, vec![upstream, m_a], shape.clone(), dtype);
let grad_b = push_node(graph, Op::Mul, vec![upstream, m_b], shape, dtype);
vec![None, Some(grad_a), Some(grad_b)]
}
}
pub struct ReluRule;
impl GradientRule for ReluRule {
fn backward(
&self,
graph: &SharedGraph,
_op: &Op,
inputs: &[NodeId],
_output: NodeId,
upstream: NodeId,
) -> GradientList {
let x = inputs[0];
let x_shape = node_shape(graph, x);
let dtype = node_dtype(graph, x);
let mask = push_node(graph, Op::Step, vec![x], x_shape.clone(), dtype);
let grad_x = push_node(graph, Op::Mul, vec![upstream, mask], x_shape, dtype);
vec![Some(grad_x)]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NodeHandle;
use fuel_ir::Shape;
use std::sync::Arc;
fn cpu_dev() -> &'static Arc<dyn fuel_backend_contract::DynBackendDevice> {
static D: std::sync::OnceLock<Arc<dyn fuel_backend_contract::DynBackendDevice>> =
std::sync::OnceLock::new();
D.get_or_init(|| Arc::new(fuel_cpu_backend::dyn_impl::CpuBackendDevice))
}
#[test]
fn dispatch_fires_for_migrated_ops() {
let a = NodeHandle::from_f32(vec![1.0_f32, 2.0, 3.0], Shape::from_dims(&[3]), cpu_dev())
.unwrap();
let b = a
.const_f32_like(vec![4.0_f32, 5.0, 6.0], Shape::from_dims(&[3]))
.unwrap();
let c = a.add(&b);
let grads = c.backward();
let ga = grads.get(&a).expect("gradient for a");
let gb = grads.get(&b).expect("gradient for b");
assert_eq!(
ga.id(),
gb.id(),
"AddRule should funnel both gradients to the same upstream node"
);
}
#[test]
fn dispatch_returns_none_for_unmigrated_ops() {
use crate::SharedGraph;
use std::sync::{Arc, RwLock};
let g: SharedGraph = Arc::new(RwLock::new(crate::Graph::new()));
let dummy = NodeId(0);
let result = dispatch_gradient(&g, &Op::MatMul, &[dummy, dummy], dummy, dummy);
assert!(
result.is_none(),
"MatMul should not have a registered rule yet"
);
}
#[test]
fn dispatch_where_returns_none_for_cond_some_for_a_and_b() {
use crate::SharedGraph;
use fuel_ir::{DType, Shape};
use std::sync::{Arc, RwLock};
let g: SharedGraph = Arc::new(RwLock::new(crate::Graph::new()));
let (cond, a, b, output, upstream) = {
let mut gw = g.write().unwrap();
let cond = gw.push(crate::Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[3]),
dtype: DType::U8,
});
let a = gw.push(crate::Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[3]),
dtype: DType::F32,
});
let b = gw.push(crate::Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[3]),
dtype: DType::F32,
});
let output = gw.push(crate::Node {
op: Op::Where,
inputs: vec![cond, a, b],
shape: Shape::from_dims(&[3]),
dtype: DType::F32,
});
let upstream = gw.push(crate::Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[3]),
dtype: DType::F32,
});
(cond, a, b, output, upstream)
};
let result = dispatch_gradient(&g, &Op::Where, &[cond, a, b], output, upstream)
.expect("Op::Where must have a registered GradientRule");
assert_eq!(result.len(), 3, "Where has 3 inputs");
assert!(
result[0].is_none(),
"cond gradient must be None (U8 non-differentiable)"
);
assert!(
result[1].is_some(),
"a gradient must be Some (differentiable through pick)"
);
assert!(
result[2].is_some(),
"b gradient must be Some (differentiable through fallback)"
);
assert_ne!(
result[1].unwrap(),
result[2].unwrap(),
"grad_a and grad_b should be distinct backward nodes"
);
}
#[test]
fn dispatch_returns_none_gradients_for_eq() {
use crate::SharedGraph;
use std::sync::{Arc, RwLock};
let g: SharedGraph = Arc::new(RwLock::new(crate::Graph::new()));
let dummy = NodeId(0);
let result = dispatch_gradient(&g, &Op::Equal, &[dummy, dummy], dummy, dummy)
.expect("Op::Equal must have a registered GradientRule");
assert_eq!(result.len(), 2, "Op::Equal has 2 inputs");
assert!(
result[0].is_none(),
"lhs gradient must be None (non-differentiable)"
);
assert!(
result[1].is_none(),
"rhs gradient must be None (non-differentiable)"
);
}
}